Merge from Chromium at DEPS revision r167172

This commit was generated by merge_to_master.py.

Change-Id: Iead6b4948cd90f0aac77a0e5e2b6c1749577569b
diff --git a/Tools/Scripts/SpacingHeuristics.pm b/Tools/Scripts/SpacingHeuristics.pm
new file mode 100644
index 0000000..7de0172
--- /dev/null
+++ b/Tools/Scripts/SpacingHeuristics.pm
@@ -0,0 +1,101 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2006 Apple Computer, Inc.  All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer. 
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution. 
+# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+#     its contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission. 
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Used for helping remove extra blank lines from files when processing.
+# see split-class for an example usage (or other scripts in bugzilla)
+
+BEGIN {
+   use Exporter   ();
+   our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
+   $VERSION     = 1.00;
+   @ISA         = qw(Exporter);
+   @EXPORT      = qw(&resetSpacingHeuristics &isOnlyWhiteSpace &applySpacingHeuristicsAndPrint &setPreviousAllowedLine &setPreviousAllowedLine &printPendingEmptyLines &ignoringLine);
+   %EXPORT_TAGS = ();
+   @EXPORT_OK   = ();
+}
+
+our @EXPORT_OK;
+
+my $justFoundEmptyLine = 0;
+my $previousLineWasDisallowed = 0;
+my $previousAllowedLine = "";
+my $pendingEmptyLines = "";
+
+sub resetSpacingHeuristics
+{
+    $justFoundEmptyLine = 0;
+    $previousLineWasDisallowed = 0;
+    $previousAllowedLine = "";
+    $pendingEmptyLines = "";
+}
+
+sub isOnlyWhiteSpace
+{
+    my $line = shift;
+    my $isOnlyWhiteSpace = ($line =~ m/^\s+$/);
+    $pendingEmptyLines .= $line if ($isOnlyWhiteSpace);
+    return $isOnlyWhiteSpace;
+}
+
+sub applySpacingHeuristicsAndPrint
+{
+    my ($out, $line) = @_;
+    
+    printPendingEmptyLines($out, $line);
+    $previousLineWasDisallowed = 0;
+    print $out $line;
+}
+
+sub setPreviousAllowedLine
+{
+    my $line = shift;
+    $previousAllowedLine = $line;
+}
+
+sub printPendingEmptyLines
+{
+    my $out = shift;
+    my $line = shift;
+    if ($previousLineWasDisallowed) {
+        if (!($pendingEmptyLines eq "") && !($previousAllowedLine =~ m/{\s*$/) && !($line =~ m/^\s*}/)) {
+            $pendingEmptyLines = "\n";
+        } else {
+            $pendingEmptyLines = "";
+        }
+    }
+    print $out $pendingEmptyLines;
+    $pendingEmptyLines = "";
+}
+
+sub ignoringLine
+{
+    # my $line = shift; # ignoring input argument
+    $previousLineWasDisallowed = 1;
+}
+
+1;
\ No newline at end of file
diff --git a/Tools/Scripts/VCSUtils.pm b/Tools/Scripts/VCSUtils.pm
new file mode 100644
index 0000000..b3b8ec2
--- /dev/null
+++ b/Tools/Scripts/VCSUtils.pm
@@ -0,0 +1,2144 @@
+# Copyright (C) 2007, 2008, 2009 Apple Inc.  All rights reserved.
+# Copyright (C) 2009, 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+# Copyright (C) 2010, 2011 Research In Motion Limited. All rights reserved.
+# Copyright (C) 2012 Daniel Bates (dbates@intudata.com)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer. 
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution. 
+# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+#     its contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission. 
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Module to share code to work with various version control systems.
+package VCSUtils;
+
+use strict;
+use warnings;
+
+use Cwd qw();  # "qw()" prevents warnings about redefining getcwd() with "use POSIX;"
+use English; # for $POSTMATCH, etc.
+use File::Basename;
+use File::Spec;
+use POSIX;
+use Term::ANSIColor qw(colored);
+
+BEGIN {
+    use Exporter   ();
+    our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
+    $VERSION     = 1.00;
+    @ISA         = qw(Exporter);
+    @EXPORT      = qw(
+        &applyGitBinaryPatchDelta
+        &callSilently
+        &canonicalizePath
+        &changeLogEmailAddress
+        &changeLogFileName
+        &changeLogName
+        &chdirReturningRelativePath
+        &decodeGitBinaryChunk
+        &decodeGitBinaryPatch
+        &determineSVNRoot
+        &determineVCSRoot
+        &escapeSubversionPath
+        &exitStatus
+        &fixChangeLogPatch
+        &gitBranch
+        &gitdiff2svndiff
+        &isGit
+        &isGitSVN
+        &isGitBranchBuild
+        &isGitDirectory
+        &isSVN
+        &isSVNDirectory
+        &isSVNVersion16OrNewer
+        &makeFilePathRelative
+        &mergeChangeLogs
+        &normalizePath
+        &parseChunkRange
+        &parseFirstEOL
+        &parsePatch
+        &pathRelativeToSVNRepositoryRootForPath
+        &possiblyColored
+        &prepareParsedPatch
+        &removeEOL
+        &runCommand
+        &runPatchCommand
+        &scmMoveOrRenameFile
+        &scmToggleExecutableBit
+        &setChangeLogDateAndReviewer
+        &svnRevisionForDirectory
+        &svnStatus
+        &toWindowsLineEndings
+        &gitCommitForSVNRevision
+        &listOfChangedFilesBetweenRevisions
+    );
+    %EXPORT_TAGS = ( );
+    @EXPORT_OK   = ();
+}
+
+our @EXPORT_OK;
+
+my $gitBranch;
+my $gitRoot;
+my $isGit;
+my $isGitSVN;
+my $isGitBranchBuild;
+my $isSVN;
+my $svnVersion;
+
+# Project time zone for Cupertino, CA, US
+my $changeLogTimeZone = "PST8PDT";
+
+my $gitDiffStartRegEx = qr#^diff --git (\w/)?(.+) (\w/)?([^\r\n]+)#;
+my $svnDiffStartRegEx = qr#^Index: ([^\r\n]+)#;
+my $svnPropertiesStartRegEx = qr#^Property changes on: ([^\r\n]+)#; # $1 is normally the same as the index path.
+my $svnPropertyStartRegEx = qr#^(Modified|Name|Added|Deleted): ([^\r\n]+)#; # $2 is the name of the property.
+my $svnPropertyValueStartRegEx = qr#^\s*(\+|-|Merged|Reverse-merged)\s*([^\r\n]+)#; # $2 is the start of the property's value (which may span multiple lines).
+my $svnPropertyValueNoNewlineRegEx = qr#\ No newline at end of property#;
+
+# This method is for portability. Return the system-appropriate exit
+# status of a child process.
+#
+# Args: pass the child error status returned by the last pipe close,
+#       for example "$?".
+sub exitStatus($)
+{
+    my ($returnvalue) = @_;
+    if ($^O eq "MSWin32") {
+        return $returnvalue >> 8;
+    }
+    if (!WIFEXITED($returnvalue)) {
+        return 254;
+    }
+    return WEXITSTATUS($returnvalue);
+}
+
+# Call a function while suppressing STDERR, and return the return values
+# as an array.
+sub callSilently($@) {
+    my ($func, @args) = @_;
+
+    # The following pattern was taken from here:
+    #   http://www.sdsc.edu/~moreland/courses/IntroPerl/docs/manual/pod/perlfunc/open.html
+    #
+    # Also see this Perl documentation (search for "open OLDERR"):
+    #   http://perldoc.perl.org/functions/open.html
+    open(OLDERR, ">&STDERR");
+    close(STDERR);
+    my @returnValue = &$func(@args);
+    open(STDERR, ">&OLDERR");
+    close(OLDERR);
+
+    return @returnValue;
+}
+
+sub toWindowsLineEndings
+{
+    my ($text) = @_;
+    $text =~ s/\n/\r\n/g;
+    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()) {
+        my $escapedDestination = escapeSubversionPath($destination);
+        my $escapedSource = escapeSubversionPath($source);
+        system("svn", "move", $escapedSource, $escapedDestination);
+    } 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
+{
+    my ($path, $executableBitDelta) = @_;
+    return if ! -e $path;
+    if ($executableBitDelta == 1) {
+        scmAddExecutableBit($path);
+    } elsif ($executableBitDelta == -1) {
+        scmRemoveExecutableBit($path);
+    }
+}
+
+sub scmAddExecutableBit($)
+{
+    my ($path) = @_;
+
+    if (isSVN()) {
+        my $escapedPath = escapeSubversionPath($path);
+        system("svn", "propset", "svn:executable", "on", $escapedPath) == 0 or die "Failed to run 'svn propset svn:executable on $escapedPath'.";
+    } elsif (isGit()) {
+        chmod(0755, $path);
+    }
+}
+
+sub scmRemoveExecutableBit($)
+{
+    my ($path) = @_;
+
+    if (isSVN()) {
+        my $escapedPath = escapeSubversionPath($path);
+        system("svn", "propdel", "svn:executable", $escapedPath) == 0 or die "Failed to run 'svn propdel svn:executable $escapedPath'.";
+    } elsif (isGit()) {
+        chmod(0664, $path);
+    }
+}
+
+sub isGitDirectory($)
+{
+    my ($dir) = @_;
+    return system("cd $dir && git rev-parse > " . File::Spec->devnull() . " 2>&1") == 0;
+}
+
+sub isGit()
+{
+    return $isGit if defined $isGit;
+
+    $isGit = isGitDirectory(".");
+    return $isGit;
+}
+
+sub isGitSVN()
+{
+    return $isGitSVN if defined $isGitSVN;
+
+    # There doesn't seem to be an officially documented way to determine
+    # if you're in a git-svn checkout. The best suggestions seen so far
+    # all use something like the following:
+    my $output = `git config --get svn-remote.svn.fetch 2>& 1`;
+    $isGitSVN = $output ne '';
+    return $isGitSVN;
+}
+
+sub gitBranch()
+{
+    unless (defined $gitBranch) {
+        chomp($gitBranch = `git symbolic-ref -q HEAD`);
+        $gitBranch = "" if exitStatus($?);
+        $gitBranch =~ s#^refs/heads/##;
+        $gitBranch = "" if $gitBranch eq "master";
+    }
+
+    return $gitBranch;
+}
+
+sub isGitBranchBuild()
+{
+    my $branch = gitBranch();
+    chomp(my $override = `git config --bool branch.$branch.webKitBranchBuild`);
+    return 1 if $override eq "true";
+    return 0 if $override eq "false";
+
+    unless (defined $isGitBranchBuild) {
+        chomp(my $gitBranchBuild = `git config --bool core.webKitBranchBuild`);
+        $isGitBranchBuild = $gitBranchBuild eq "true";
+    }
+
+    return $isGitBranchBuild;
+}
+
+sub isSVNDirectory($)
+{
+    my ($dir) = @_;
+    return system("cd $dir && svn info > " . File::Spec->devnull() . " 2>&1") == 0;
+}
+
+sub isSVN()
+{
+    return $isSVN if defined $isSVN;
+
+    $isSVN = isSVNDirectory(".");
+    return $isSVN;
+}
+
+sub svnVersion()
+{
+    return $svnVersion if defined $svnVersion;
+
+    if (!isSVN()) {
+        $svnVersion = 0;
+    } else {
+        chomp($svnVersion = `svn --version --quiet`);
+    }
+    return $svnVersion;
+}
+
+sub isSVNVersion16OrNewer()
+{
+    my $version = svnVersion();
+    return eval "v$version" ge v1.6;
+}
+
+sub chdirReturningRelativePath($)
+{
+    my ($directory) = @_;
+    my $previousDirectory = Cwd::getcwd();
+    chdir $directory;
+    my $newDirectory = Cwd::getcwd();
+    return "." if $newDirectory eq $previousDirectory;
+    return File::Spec->abs2rel($previousDirectory, $newDirectory);
+}
+
+sub determineGitRoot()
+{
+    chomp(my $gitDir = `git rev-parse --git-dir`);
+    return dirname($gitDir);
+}
+
+sub determineSVNRoot()
+{
+    my $last = '';
+    my $path = '.';
+    my $parent = '..';
+    my $repositoryRoot;
+    my $repositoryUUID;
+    while (1) {
+        my $thisRoot;
+        my $thisUUID;
+        my $escapedPath = escapeSubversionPath($path);
+        # Ignore error messages in case we've run past the root of the checkout.
+        open INFO, "svn info '$escapedPath' 2> " . File::Spec->devnull() . " |" or die;
+        while (<INFO>) {
+            if (/^Repository Root: (.+)/) {
+                $thisRoot = $1;
+            }
+            if (/^Repository UUID: (.+)/) {
+                $thisUUID = $1;
+            }
+            if ($thisRoot && $thisUUID) {
+                local $/ = undef;
+                <INFO>; # Consume the rest of the input.
+            }
+        }
+        close INFO;
+
+        # It's possible (e.g. for developers of some ports) to have a WebKit
+        # checkout in a subdirectory of another checkout.  So abort if the
+        # repository root or the repository UUID suddenly changes.
+        last if !$thisUUID;
+        $repositoryUUID = $thisUUID if !$repositoryUUID;
+        last if $thisUUID ne $repositoryUUID;
+
+        last if !$thisRoot;
+        $repositoryRoot = $thisRoot if !$repositoryRoot;
+        last if $thisRoot ne $repositoryRoot;
+
+        $last = $path;
+        $path = File::Spec->catdir($parent, $path);
+    }
+
+    return File::Spec->rel2abs($last);
+}
+
+sub determineVCSRoot()
+{
+    if (isGit()) {
+        return determineGitRoot();
+    }
+
+    if (!isSVN()) {
+        # Some users have a workflow where svn-create-patch, svn-apply and
+        # svn-unapply are used outside of multiple svn working directores,
+        # so warn the user and assume Subversion is being used in this case.
+        warn "Unable to determine VCS root for '" . Cwd::getcwd() . "'; assuming Subversion";
+        $isSVN = 1;
+    }
+
+    return determineSVNRoot();
+}
+
+sub isWindows()
+{
+    return ($^O eq "MSWin32") || 0;
+}
+
+sub svnRevisionForDirectory($)
+{
+    my ($dir) = @_;
+    my $revision;
+
+    if (isSVNDirectory($dir)) {
+        my $escapedDir = escapeSubversionPath($dir);
+        my $command = "svn info $escapedDir | grep Revision:";
+        $command = "LC_ALL=C $command" if !isWindows();
+        my $svnInfo = `$command`;
+        ($revision) = ($svnInfo =~ m/Revision: (\d+).*/g);
+    } elsif (isGitDirectory($dir)) {
+        my $command = "git log --grep=\"git-svn-id: \" -n 1 | grep git-svn-id:";
+        $command = "LC_ALL=C $command" if !isWindows();
+        $command = "cd $dir && $command";
+        my $gitLog = `$command`;
+        ($revision) = ($gitLog =~ m/ +git-svn-id: .+@(\d+) /g);
+    }
+    if (!defined($revision)) {
+        $revision = "unknown";
+        warn "Unable to determine current SVN revision in $dir";
+    }
+    return $revision;
+}
+
+sub pathRelativeToSVNRepositoryRootForPath($)
+{
+    my ($file) = @_;
+    my $relativePath = File::Spec->abs2rel($file);
+
+    my $svnInfo;
+    if (isSVN()) {
+        my $escapedRelativePath = escapeSubversionPath($relativePath);
+        my $command = "svn info $escapedRelativePath";
+        $command = "LC_ALL=C $command" if !isWindows();
+        $svnInfo = `$command`;
+    } elsif (isGit()) {
+        my $command = "git svn info $relativePath";
+        $command = "LC_ALL=C $command" if !isWindows();
+        $svnInfo = `$command`;
+    }
+
+    $svnInfo =~ /.*^URL: (.*?)$/m;
+    my $svnURL = $1;
+
+    $svnInfo =~ /.*^Repository Root: (.*?)$/m;
+    my $repositoryRoot = $1;
+
+    $svnURL =~ s/$repositoryRoot\///;
+    return $svnURL;
+}
+
+sub makeFilePathRelative($)
+{
+    my ($path) = @_;
+    return $path unless isGit();
+
+    unless (defined $gitRoot) {
+        chomp($gitRoot = `git rev-parse --show-cdup`);
+    }
+    return $gitRoot . $path;
+}
+
+sub normalizePath($)
+{
+    my ($path) = @_;
+    $path =~ s/\\/\//g;
+    return $path;
+}
+
+sub possiblyColored($$)
+{
+    my ($colors, $string) = @_;
+
+    if (-t STDOUT) {
+        return colored([$colors], $string);
+    } else {
+        return $string;
+    }
+}
+
+sub adjustPathForRecentRenamings($) 
+{ 
+    my ($fullPath) = @_; 
+ 
+    $fullPath =~ s|WebCore/webaudio|WebCore/Modules/webaudio|g;
+    $fullPath =~ s|JavaScriptCore/wtf|WTF/wtf|g;
+    $fullPath =~ s|test_expectations.txt|TestExpectations|g;
+
+    return $fullPath; 
+} 
+
+sub canonicalizePath($)
+{
+    my ($file) = @_;
+
+    # Remove extra slashes and '.' directories in path
+    $file = File::Spec->canonpath($file);
+
+    # Remove '..' directories in path
+    my @dirs = ();
+    foreach my $dir (File::Spec->splitdir($file)) {
+        if ($dir eq '..' && $#dirs >= 0 && $dirs[$#dirs] ne '..') {
+            pop(@dirs);
+        } else {
+            push(@dirs, $dir);
+        }
+    }
+    return ($#dirs >= 0) ? File::Spec->catdir(@dirs) : ".";
+}
+
+sub removeEOL($)
+{
+    my ($line) = @_;
+    return "" unless $line;
+
+    $line =~ s/[\r\n]+$//g;
+    return $line;
+}
+
+sub parseFirstEOL($)
+{
+    my ($fileHandle) = @_;
+
+    # Make input record separator the new-line character to simplify regex matching below.
+    my $savedInputRecordSeparator = $INPUT_RECORD_SEPARATOR;
+    $INPUT_RECORD_SEPARATOR = "\n";
+    my $firstLine  = <$fileHandle>;
+    $INPUT_RECORD_SEPARATOR = $savedInputRecordSeparator;
+
+    return unless defined($firstLine);
+
+    my $eol;
+    if ($firstLine =~ /\r\n/) {
+        $eol = "\r\n";
+    } elsif ($firstLine =~ /\r/) {
+        $eol = "\r";
+    } elsif ($firstLine =~ /\n/) {
+        $eol = "\n";
+    }
+    return $eol;
+}
+
+sub firstEOLInFile($)
+{
+    my ($file) = @_;
+    my $eol;
+    if (open(FILE, $file)) {
+        $eol = parseFirstEOL(*FILE);
+        close(FILE);
+    }
+    return $eol;
+}
+
+# Parses a chunk range line into its components.
+#
+# A chunk range line has the form: @@ -L_1,N_1 +L_2,N_2 @@, where the pairs (L_1, N_1),
+# (L_2, N_2) are ranges that represent the starting line number and line count in the
+# original file and new file, respectively.
+#
+# Note, some versions of GNU diff may omit the comma and trailing line count (e.g. N_1),
+# in which case the omitted line count defaults to 1. For example, GNU diff may output
+# @@ -1 +1 @@, which is equivalent to @@ -1,1 +1,1 @@.
+#
+# This subroutine returns undef if given an invalid or malformed chunk range.
+#
+# Args:
+#   $line: the line to parse.
+#   $chunkSentinel: the sentinel that surrounds the chunk range information (defaults to "@@").
+#
+# Returns $chunkRangeHashRef
+#   $chunkRangeHashRef: a hash reference representing the parts of a chunk range, as follows--
+#     startingLine: the starting line in the original file.
+#     lineCount: the line count in the original file.
+#     newStartingLine: the new starting line in the new file.
+#     newLineCount: the new line count in the new file.
+sub parseChunkRange($;$)
+{
+    my ($line, $chunkSentinel) = @_;
+    $chunkSentinel = "@@" if !$chunkSentinel;
+    my $chunkRangeRegEx = qr#^\Q$chunkSentinel\E -(\d+)(,(\d+))? \+(\d+)(,(\d+))? \Q$chunkSentinel\E#;
+    if ($line !~ /$chunkRangeRegEx/) {
+        return;
+    }
+    my %chunkRange;
+    $chunkRange{startingLine} = $1;
+    $chunkRange{lineCount} = defined($2) ? $3 : 1;
+    $chunkRange{newStartingLine} = $4;
+    $chunkRange{newLineCount} = defined($5) ? $6 : 1;
+    return \%chunkRange;
+}
+
+sub svnStatus($)
+{
+    my ($fullPath) = @_;
+    my $escapedFullPath = escapeSubversionPath($fullPath);
+    my $svnStatus;
+    open SVN, "svn status --non-interactive --non-recursive '$escapedFullPath' |" or die;
+    if (-d $fullPath) {
+        # When running "svn stat" on a directory, we can't assume that only one
+        # status will be returned (since any files with a status below the
+        # directory will be returned), and we can't assume that the directory will
+        # be first (since any files with unknown status will be listed first).
+        my $normalizedFullPath = File::Spec->catdir(File::Spec->splitdir($fullPath));
+        while (<SVN>) {
+            # Input may use a different EOL sequence than $/, so avoid chomp.
+            $_ = removeEOL($_);
+            my $normalizedStatPath = File::Spec->catdir(File::Spec->splitdir(substr($_, 7)));
+            if ($normalizedFullPath eq $normalizedStatPath) {
+                $svnStatus = "$_\n";
+                last;
+            }
+        }
+        # Read the rest of the svn command output to avoid a broken pipe warning.
+        local $/ = undef;
+        <SVN>;
+    }
+    else {
+        # Files will have only one status returned.
+        $svnStatus = removeEOL(<SVN>) . "\n";
+    }
+    close SVN;
+    return $svnStatus;
+}
+
+# Return whether the given file mode is executable in the source control
+# sense.  We make this determination based on whether the executable bit
+# is set for "others" rather than the stronger condition that it be set
+# for the user, group, and others.  This is sufficient for distinguishing
+# the default behavior in Git and SVN.
+#
+# Args:
+#   $fileMode: A number or string representing a file mode in octal notation.
+sub isExecutable($)
+{
+    my $fileMode = shift;
+
+    return $fileMode % 2;
+}
+
+# Parse the next Git diff header from the given file handle, and advance
+# the handle so the last line read is the first line after the header.
+#
+# This subroutine dies if given leading junk.
+#
+# Args:
+#   $fileHandle: advanced so the last line read from the handle is the first
+#                line of the header to parse.  This should be a line
+#                beginning with "diff --git".
+#   $line: the line last read from $fileHandle
+#
+# Returns ($headerHashRef, $lastReadLine):
+#   $headerHashRef: a hash reference representing a diff header, as follows--
+#     copiedFromPath: the path from which the file was copied or moved if
+#                     the diff is a copy or move.
+#     executableBitDelta: the value 1 or -1 if the executable bit was added or
+#                         removed, respectively.  New and deleted files have
+#                         this value only if the file is executable, in which
+#                         case the value is 1 and -1, respectively.
+#     indexPath: the path of the target file.
+#     isBinary: the value 1 if the diff is for a binary file.
+#     isDeletion: the value 1 if the diff is a file deletion.
+#     isCopyWithChanges: the value 1 if the file was copied or moved and
+#                        the target file was changed in some way after being
+#                        copied or moved (e.g. if its contents or executable
+#                        bit were changed).
+#     isNew: the value 1 if the diff is for a new file.
+#     shouldDeleteSource: the value 1 if the file was copied or moved and
+#                         the source file was deleted -- i.e. if the copy
+#                         was actually a move.
+#     svnConvertedText: the header text with some lines converted to SVN
+#                       format.  Git-specific lines are preserved.
+#   $lastReadLine: the line last read from $fileHandle.
+sub parseGitDiffHeader($$)
+{
+    my ($fileHandle, $line) = @_;
+
+    $_ = $line;
+
+    my $indexPath;
+    if (/$gitDiffStartRegEx/) {
+        # The first and second paths can differ in the case of copies
+        # and renames.  We use the second file path because it is the
+        # destination path.
+        $indexPath = adjustPathForRecentRenamings($4);
+        # Use $POSTMATCH to preserve the end-of-line character.
+        $_ = "Index: $indexPath$POSTMATCH"; # Convert to SVN format.
+    } else {
+        die("Could not parse leading \"diff --git\" line: \"$line\".");
+    }
+
+    my $copiedFromPath;
+    my $foundHeaderEnding;
+    my $isBinary;
+    my $isDeletion;
+    my $isNew;
+    my $newExecutableBit = 0;
+    my $oldExecutableBit = 0;
+    my $shouldDeleteSource = 0;
+    my $similarityIndex = 0;
+    my $svnConvertedText;
+    while (1) {
+        # Temporarily strip off any end-of-line characters to simplify
+        # regex matching below.
+        s/([\n\r]+)$//;
+        my $eol = $1;
+
+        if (/^(deleted file|old) mode (\d+)/) {
+            $oldExecutableBit = (isExecutable($2) ? 1 : 0);
+            $isDeletion = 1 if $1 eq "deleted file";
+        } elsif (/^new( file)? mode (\d+)/) {
+            $newExecutableBit = (isExecutable($2) ? 1 : 0);
+            $isNew = 1 if $1;
+        } elsif (/^similarity index (\d+)%/) {
+            $similarityIndex = $1;
+        } elsif (/^copy from (\S+)/) {
+            $copiedFromPath = $1;
+        } elsif (/^rename from (\S+)/) {
+            # FIXME: Record this as a move rather than as a copy-and-delete.
+            #        This will simplify adding rename support to svn-unapply.
+            #        Otherwise, the hash for a deletion would have to know
+            #        everything about the file being deleted in order to
+            #        support undoing itself.  Recording as a move will also
+            #        permit us to use "svn move" and "git move".
+            $copiedFromPath = $1;
+            $shouldDeleteSource = 1;
+        } elsif (/^--- \S+/) {
+            $_ = "--- $indexPath"; # Convert to SVN format.
+        } elsif (/^\+\+\+ \S+/) {
+            $_ = "+++ $indexPath"; # Convert to SVN format.
+            $foundHeaderEnding = 1;
+        } elsif (/^GIT binary patch$/ ) {
+            $isBinary = 1;
+            $foundHeaderEnding = 1;
+        # The "git diff" command includes a line of the form "Binary files
+        # <path1> and <path2> differ" if the --binary flag is not used.
+        } elsif (/^Binary files / ) {
+            die("Error: the Git diff contains a binary file without the binary data in ".
+                "line: \"$_\".  Be sure to use the --binary flag when invoking \"git diff\" ".
+                "with diffs containing binary files.");
+        }
+
+        $svnConvertedText .= "$_$eol"; # Also restore end-of-line characters.
+
+        $_ = <$fileHandle>; # Not defined if end-of-file reached.
+
+        last if (!defined($_) || /$gitDiffStartRegEx/ || $foundHeaderEnding);
+    }
+
+    my $executableBitDelta = $newExecutableBit - $oldExecutableBit;
+
+    my %header;
+
+    $header{copiedFromPath} = $copiedFromPath if $copiedFromPath;
+    $header{executableBitDelta} = $executableBitDelta if $executableBitDelta;
+    $header{indexPath} = $indexPath;
+    $header{isBinary} = $isBinary if $isBinary;
+    $header{isCopyWithChanges} = 1 if ($copiedFromPath && ($similarityIndex != 100 || $executableBitDelta));
+    $header{isDeletion} = $isDeletion if $isDeletion;
+    $header{isNew} = $isNew if $isNew;
+    $header{shouldDeleteSource} = $shouldDeleteSource if $shouldDeleteSource;
+    $header{svnConvertedText} = $svnConvertedText;
+
+    return (\%header, $_);
+}
+
+# Parse the next SVN diff header from the given file handle, and advance
+# the handle so the last line read is the first line after the header.
+#
+# This subroutine dies if given leading junk or if it could not detect
+# the end of the header block.
+#
+# Args:
+#   $fileHandle: advanced so the last line read from the handle is the first
+#                line of the header to parse.  This should be a line
+#                beginning with "Index:".
+#   $line: the line last read from $fileHandle
+#
+# Returns ($headerHashRef, $lastReadLine):
+#   $headerHashRef: a hash reference representing a diff header, as follows--
+#     copiedFromPath: the path from which the file was copied if the diff
+#                     is a copy.
+#     indexPath: the path of the target file, which is the path found in
+#                the "Index:" line.
+#     isBinary: the value 1 if the diff is for a binary file.
+#     isNew: the value 1 if the diff is for a new file.
+#     sourceRevision: the revision number of the source, if it exists.  This
+#                     is the same as the revision number the file was copied
+#                     from, in the case of a file copy.
+#     svnConvertedText: the header text converted to a header with the paths
+#                       in some lines corrected.
+#   $lastReadLine: the line last read from $fileHandle.
+sub parseSvnDiffHeader($$)
+{
+    my ($fileHandle, $line) = @_;
+
+    $_ = $line;
+
+    my $indexPath;
+    if (/$svnDiffStartRegEx/) {
+        $indexPath = adjustPathForRecentRenamings($1);
+    } else {
+        die("First line of SVN diff does not begin with \"Index \": \"$_\"");
+    }
+
+    my $copiedFromPath;
+    my $foundHeaderEnding;
+    my $isBinary;
+    my $isNew;
+    my $sourceRevision;
+    my $svnConvertedText;
+    while (1) {
+        # Temporarily strip off any end-of-line characters to simplify
+        # regex matching below.
+        s/([\n\r]+)$//;
+        my $eol = $1;
+
+        # Fix paths on "---" and "+++" lines to match the leading
+        # index line.
+        if (s/^--- [^\t\n\r]+/--- $indexPath/) {
+            # ---
+            if (/^--- .+\(revision (\d+)\)/) {
+                $sourceRevision = $1;
+                $isNew = 1 if !$sourceRevision; # if revision 0.
+                if (/\(from (\S+):(\d+)\)$/) {
+                    # The "from" clause is created by svn-create-patch, in
+                    # which case there is always also a "revision" clause.
+                    $copiedFromPath = $1;
+                    die("Revision number \"$2\" in \"from\" clause does not match " .
+                        "source revision number \"$sourceRevision\".") if ($2 != $sourceRevision);
+                }
+            }
+        } elsif (s/^\+\+\+ [^\t\n\r]+/+++ $indexPath/ || $isBinary && /^$/) {
+            $foundHeaderEnding = 1;
+        } elsif (/^Cannot display: file marked as a binary type.$/) {
+            $isBinary = 1;
+            # SVN 1.7 has an unusual display format for a binary diff. It repeats the first
+            # two lines of the diff header. For example:
+            #     Index: test_file.swf
+            #     ===================================================================
+            #     Cannot display: file marked as a binary type.
+            #     svn:mime-type = application/octet-stream
+            #     Index: test_file.swf
+            #     ===================================================================
+            #     --- test_file.swf
+            #     +++ test_file.swf
+            #
+            #     ...
+            #     Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+            # Therefore, we continue reading the diff header until we either encounter a line
+            # that begins with "+++" (SVN 1.7 or greater) or an empty line (SVN version less
+            # than 1.7).
+        }
+
+        $svnConvertedText .= "$_$eol"; # Also restore end-of-line characters.
+
+        $_ = <$fileHandle>; # Not defined if end-of-file reached.
+
+        last if (!defined($_) || !$isBinary && /$svnDiffStartRegEx/ || $foundHeaderEnding);
+    }
+
+    if (!$foundHeaderEnding) {
+        die("Did not find end of header block corresponding to index path \"$indexPath\".");
+    }
+
+    my %header;
+
+    $header{copiedFromPath} = $copiedFromPath if $copiedFromPath;
+    $header{indexPath} = $indexPath;
+    $header{isBinary} = $isBinary if $isBinary;
+    $header{isNew} = $isNew if $isNew;
+    $header{sourceRevision} = $sourceRevision if $sourceRevision;
+    $header{svnConvertedText} = $svnConvertedText;
+
+    return (\%header, $_);
+}
+
+# Parse the next diff header from the given file handle, and advance
+# the handle so the last line read is the first line after the header.
+#
+# This subroutine dies if given leading junk or if it could not detect
+# the end of the header block.
+#
+# Args:
+#   $fileHandle: advanced so the last line read from the handle is the first
+#                line of the header to parse.  For SVN-formatted diffs, this
+#                is a line beginning with "Index:".  For Git, this is a line
+#                beginning with "diff --git".
+#   $line: the line last read from $fileHandle
+#
+# Returns ($headerHashRef, $lastReadLine):
+#   $headerHashRef: a hash reference representing a diff header
+#     copiedFromPath: the path from which the file was copied if the diff
+#                     is a copy.
+#     executableBitDelta: the value 1 or -1 if the executable bit was added or
+#                         removed, respectively.  New and deleted files have
+#                         this value only if the file is executable, in which
+#                         case the value is 1 and -1, respectively.
+#     indexPath: the path of the target file.
+#     isBinary: the value 1 if the diff is for a binary file.
+#     isGit: the value 1 if the diff is Git-formatted.
+#     isSvn: the value 1 if the diff is SVN-formatted.
+#     sourceRevision: the revision number of the source, if it exists.  This
+#                     is the same as the revision number the file was copied
+#                     from, in the case of a file copy.
+#     svnConvertedText: the header text with some lines converted to SVN
+#                       format.  Git-specific lines are preserved.
+#   $lastReadLine: the line last read from $fileHandle.
+sub parseDiffHeader($$)
+{
+    my ($fileHandle, $line) = @_;
+
+    my $header;  # This is a hash ref.
+    my $isGit;
+    my $isSvn;
+    my $lastReadLine;
+
+    if ($line =~ $svnDiffStartRegEx) {
+        $isSvn = 1;
+        ($header, $lastReadLine) = parseSvnDiffHeader($fileHandle, $line);
+    } elsif ($line =~ $gitDiffStartRegEx) {
+        $isGit = 1;
+        ($header, $lastReadLine) = parseGitDiffHeader($fileHandle, $line);
+    } else {
+        die("First line of diff does not begin with \"Index:\" or \"diff --git\": \"$line\"");
+    }
+
+    $header->{isGit} = $isGit if $isGit;
+    $header->{isSvn} = $isSvn if $isSvn;
+
+    return ($header, $lastReadLine);
+}
+
+# FIXME: The %diffHash "object" should not have an svnConvertedText property.
+#        Instead, the hash object should store its information in a
+#        structured way as properties.  This should be done in a way so
+#        that, if necessary, the text of an SVN or Git patch can be
+#        reconstructed from the information in those hash properties.
+#
+# A %diffHash is a hash representing a source control diff of a single
+# file operation (e.g. a file modification, copy, or delete).
+#
+# These hashes appear, for example, in the parseDiff(), parsePatch(),
+# and prepareParsedPatch() subroutines of this package.
+#
+# The corresponding values are--
+#
+#   copiedFromPath: the path from which the file was copied if the diff
+#                   is a copy.
+#   executableBitDelta: the value 1 or -1 if the executable bit was added or
+#                       removed from the target file, respectively.
+#   indexPath: the path of the target file.  For SVN-formatted diffs,
+#              this is the same as the path in the "Index:" line.
+#   isBinary: the value 1 if the diff is for a binary file.
+#   isDeletion: the value 1 if the diff is known from the header to be a deletion.
+#   isGit: the value 1 if the diff is Git-formatted.
+#   isNew: the value 1 if the dif is known from the header to be a new file.
+#   isSvn: the value 1 if the diff is SVN-formatted.
+#   sourceRevision: the revision number of the source, if it exists.  This
+#                   is the same as the revision number the file was copied
+#                   from, in the case of a file copy.
+#   svnConvertedText: the diff with some lines converted to SVN format.
+#                     Git-specific lines are preserved.
+
+# Parse one diff from a patch file created by svn-create-patch, and
+# advance the file handle so the last line read is the first line
+# of the next header block.
+#
+# This subroutine preserves any leading junk encountered before the header.
+#
+# Composition of an SVN diff
+#
+# There are three parts to an SVN diff: the header, the property change, and
+# the binary contents, in that order. Either the header or the property change
+# may be ommitted, but not both. If there are binary changes, then you always
+# have all three.
+#
+# Args:
+#   $fileHandle: a file handle advanced to the first line of the next
+#                header block. Leading junk is okay.
+#   $line: the line last read from $fileHandle.
+#   $optionsHashRef: a hash reference representing optional options to use
+#                    when processing a diff.
+#     shouldNotUseIndexPathEOL: whether to use the line endings in the diff instead
+#                               instead of the line endings in the target file; the
+#                               value of 1 if svnConvertedText should use the line
+#                               endings in the diff.
+#
+# Returns ($diffHashRefs, $lastReadLine):
+#   $diffHashRefs: A reference to an array of references to %diffHash hashes.
+#                  See the %diffHash documentation above.
+#   $lastReadLine: the line last read from $fileHandle
+sub parseDiff($$;$)
+{
+    # FIXME: Adjust this method so that it dies if the first line does not
+    #        match the start of a diff.  This will require a change to
+    #        parsePatch() so that parsePatch() skips over leading junk.
+    my ($fileHandle, $line, $optionsHashRef) = @_;
+
+    my $headerStartRegEx = $svnDiffStartRegEx; # SVN-style header for the default
+
+    my $headerHashRef; # Last header found, as returned by parseDiffHeader().
+    my $svnPropertiesHashRef; # Last SVN properties diff found, as returned by parseSvnDiffProperties().
+    my $svnText;
+    my $indexPathEOL;
+    my $numTextChunks = 0;
+    while (defined($line)) {
+        if (!$headerHashRef && ($line =~ $gitDiffStartRegEx)) {
+            # Then assume all diffs in the patch are Git-formatted. This
+            # block was made to be enterable at most once since we assume
+            # all diffs in the patch are formatted the same (SVN or Git).
+            $headerStartRegEx = $gitDiffStartRegEx;
+        }
+
+        if ($line =~ $svnPropertiesStartRegEx) {
+            my $propertyPath = $1;
+            if ($svnPropertiesHashRef || $headerHashRef && ($propertyPath ne $headerHashRef->{indexPath})) {
+                # This is the start of the second diff in the while loop, which happens to
+                # be a property diff.  If $svnPropertiesHasRef is defined, then this is the
+                # second consecutive property diff, otherwise it's the start of a property
+                # diff for a file that only has property changes.
+                last;
+            }
+            ($svnPropertiesHashRef, $line) = parseSvnDiffProperties($fileHandle, $line);
+            next;
+        }
+        if ($line !~ $headerStartRegEx) {
+            # Then we are in the body of the diff.
+            my $isChunkRange = defined(parseChunkRange($line));
+            $numTextChunks += 1 if $isChunkRange;
+            if ($indexPathEOL && !$isChunkRange) {
+                # The chunk range is part of the body of the diff, but its line endings should't be
+                # modified or patch(1) will complain. So, we only modify non-chunk range lines.
+                $line =~ s/\r\n|\r|\n/$indexPathEOL/g;
+            }
+            $svnText .= $line;
+            $line = <$fileHandle>;
+            next;
+        } # Otherwise, we found a diff header.
+
+        if ($svnPropertiesHashRef || $headerHashRef) {
+            # Then either we just processed an SVN property change or this
+            # is the start of the second diff header of this while loop.
+            last;
+        }
+
+        ($headerHashRef, $line) = parseDiffHeader($fileHandle, $line);
+        if (!$optionsHashRef || !$optionsHashRef->{shouldNotUseIndexPathEOL}) {
+            $indexPathEOL = firstEOLInFile($headerHashRef->{indexPath}) if !$headerHashRef->{isNew} && !$headerHashRef->{isBinary};
+        }
+
+        $svnText .= $headerHashRef->{svnConvertedText};
+    }
+
+    my @diffHashRefs;
+
+    if ($headerHashRef->{shouldDeleteSource}) {
+        my %deletionHash;
+        $deletionHash{indexPath} = $headerHashRef->{copiedFromPath};
+        $deletionHash{isDeletion} = 1;
+        push @diffHashRefs, \%deletionHash;
+    }
+    if ($headerHashRef->{copiedFromPath}) {
+        my %copyHash;
+        $copyHash{copiedFromPath} = $headerHashRef->{copiedFromPath};
+        $copyHash{indexPath} = $headerHashRef->{indexPath};
+        $copyHash{sourceRevision} = $headerHashRef->{sourceRevision} if $headerHashRef->{sourceRevision};
+        if ($headerHashRef->{isSvn}) {
+            $copyHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta};
+        }
+        push @diffHashRefs, \%copyHash;
+    }
+
+    # Note, the order of evaluation for the following if conditional has been explicitly chosen so that
+    # it evaluates to false when there is no headerHashRef (e.g. a property change diff for a file that
+    # only has property changes).
+    if ($headerHashRef->{isCopyWithChanges} || (%$headerHashRef && !$headerHashRef->{copiedFromPath})) {
+        # Then add the usual file modification.
+        my %diffHash;
+        # FIXME: We should expand this code to support other properties.  In the future,
+        #        parseSvnDiffProperties may return a hash whose keys are the properties.
+        if ($headerHashRef->{isSvn}) {
+            # SVN records the change to the executable bit in a separate property change diff
+            # that follows the contents of the diff, except for binary diffs.  For binary
+            # diffs, the property change diff follows the diff header.
+            $diffHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta};
+        } elsif ($headerHashRef->{isGit}) {
+            # Git records the change to the executable bit in the header of a diff.
+            $diffHash{executableBitDelta} = $headerHashRef->{executableBitDelta} if $headerHashRef->{executableBitDelta};
+        }
+        $diffHash{indexPath} = $headerHashRef->{indexPath};
+        $diffHash{isBinary} = $headerHashRef->{isBinary} if $headerHashRef->{isBinary};
+        $diffHash{isDeletion} = $headerHashRef->{isDeletion} if $headerHashRef->{isDeletion};
+        $diffHash{isGit} = $headerHashRef->{isGit} if $headerHashRef->{isGit};
+        $diffHash{isNew} = $headerHashRef->{isNew} if $headerHashRef->{isNew};
+        $diffHash{isSvn} = $headerHashRef->{isSvn} if $headerHashRef->{isSvn};
+        if (!$headerHashRef->{copiedFromPath}) {
+            # If the file was copied, then we have already incorporated the
+            # sourceRevision information into the change.
+            $diffHash{sourceRevision} = $headerHashRef->{sourceRevision} if $headerHashRef->{sourceRevision};
+        }
+        # FIXME: Remove the need for svnConvertedText.  See the %diffHash
+        #        code comments above for more information.
+        #
+        # Note, we may not always have SVN converted text since we intend
+        # to deprecate it in the future.  For example, a property change
+        # diff for a file that only has property changes will not return
+        # any SVN converted text.
+        $diffHash{svnConvertedText} = $svnText if $svnText;
+        $diffHash{numTextChunks} = $numTextChunks if $svnText && !$headerHashRef->{isBinary};
+        push @diffHashRefs, \%diffHash;
+    }
+
+    if (!%$headerHashRef && $svnPropertiesHashRef) {
+        # A property change diff for a file that only has property changes.
+        my %propertyChangeHash;
+        $propertyChangeHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta};
+        $propertyChangeHash{indexPath} = $svnPropertiesHashRef->{propertyPath};
+        $propertyChangeHash{isSvn} = 1;
+        push @diffHashRefs, \%propertyChangeHash;
+    }
+
+    return (\@diffHashRefs, $line);
+}
+
+# Parse an SVN property change diff from the given file handle, and advance
+# the handle so the last line read is the first line after this diff.
+#
+# For the case of an SVN binary diff, the binary contents will follow the
+# the property changes.
+#
+# This subroutine dies if the first line does not begin with "Property changes on"
+# or if the separator line that follows this line is missing.
+#
+# Args:
+#   $fileHandle: advanced so the last line read from the handle is the first
+#                line of the footer to parse.  This line begins with
+#                "Property changes on".
+#   $line: the line last read from $fileHandle.
+#
+# Returns ($propertyHashRef, $lastReadLine):
+#   $propertyHashRef: a hash reference representing an SVN diff footer.
+#     propertyPath: the path of the target file.
+#     executableBitDelta: the value 1 or -1 if the executable bit was added or
+#                         removed from the target file, respectively.
+#   $lastReadLine: the line last read from $fileHandle.
+sub parseSvnDiffProperties($$)
+{
+    my ($fileHandle, $line) = @_;
+
+    $_ = $line;
+
+    my %footer;
+    if (/$svnPropertiesStartRegEx/) {
+        $footer{propertyPath} = $1;
+    } else {
+        die("Failed to find start of SVN property change, \"Property changes on \": \"$_\"");
+    }
+
+    # We advance $fileHandle two lines so that the next line that
+    # we process is $svnPropertyStartRegEx in a well-formed footer.
+    # A well-formed footer has the form:
+    # Property changes on: FileA
+    # ___________________________________________________________________
+    # Added: svn:executable
+    #    + *
+    $_ = <$fileHandle>; # Not defined if end-of-file reached.
+    my $separator = "_" x 67;
+    if (defined($_) && /^$separator[\r\n]+$/) {
+        $_ = <$fileHandle>;
+    } else {
+        die("Failed to find separator line: \"$_\".");
+    }
+
+    # FIXME: We should expand this to support other SVN properties
+    #        (e.g. return a hash of property key-values that represents
+    #        all properties).
+    #
+    # Notice, we keep processing until we hit end-of-file or some
+    # line that does not resemble $svnPropertyStartRegEx, such as
+    # the empty line that precedes the start of the binary contents
+    # of a patch, or the start of the next diff (e.g. "Index:").
+    my $propertyHashRef;
+    while (defined($_) && /$svnPropertyStartRegEx/) {
+        ($propertyHashRef, $_) = parseSvnProperty($fileHandle, $_);
+        if ($propertyHashRef->{name} eq "svn:executable") {
+            # Notice, for SVN properties, propertyChangeDelta is always non-zero
+            # because a property can only be added or removed.
+            $footer{executableBitDelta} = $propertyHashRef->{propertyChangeDelta};   
+        }
+    }
+
+    return(\%footer, $_);
+}
+
+# Parse the next SVN property from the given file handle, and advance the handle so the last
+# line read is the first line after the property.
+#
+# This subroutine dies if the first line is not a valid start of an SVN property,
+# or the property is missing a value, or the property change type (e.g. "Added")
+# does not correspond to the property value type (e.g. "+").
+#
+# Args:
+#   $fileHandle: advanced so the last line read from the handle is the first
+#                line of the property to parse.  This should be a line
+#                that matches $svnPropertyStartRegEx.
+#   $line: the line last read from $fileHandle.
+#
+# Returns ($propertyHashRef, $lastReadLine):
+#   $propertyHashRef: a hash reference representing a SVN property.
+#     name: the name of the property.
+#     value: the last property value.  For instance, suppose the property is "Modified".
+#            Then it has both a '-' and '+' property value in that order.  Therefore,
+#            the value of this key is the value of the '+' property by ordering (since
+#            it is the last value).
+#     propertyChangeDelta: the value 1 or -1 if the property was added or
+#                          removed, respectively.
+#   $lastReadLine: the line last read from $fileHandle.
+sub parseSvnProperty($$)
+{
+    my ($fileHandle, $line) = @_;
+
+    $_ = $line;
+
+    my $propertyName;
+    my $propertyChangeType;
+    if (/$svnPropertyStartRegEx/) {
+        $propertyChangeType = $1;
+        $propertyName = $2;
+    } else {
+        die("Failed to find SVN property: \"$_\".");
+    }
+
+    $_ = <$fileHandle>; # Not defined if end-of-file reached.
+
+    if (defined($_) && defined(parseChunkRange($_, "##"))) {
+        # FIXME: We should validate the chunk range line that is part of an SVN 1.7
+        #        property diff. For now, we ignore this line.
+        $_ = <$fileHandle>;
+    }
+
+    # The "svn diff" command neither inserts newline characters between property values
+    # nor between successive properties.
+    #
+    # As of SVN 1.7, "svn diff" may insert "\ No newline at end of property" after a
+    # property value that doesn't end in a newline.
+    #
+    # FIXME: We do not support property values that contain tailing newline characters
+    #        as it is difficult to disambiguate these trailing newlines from the empty
+    #        line that precedes the contents of a binary patch.
+    my $propertyValue;
+    my $propertyValueType;
+    while (defined($_) && /$svnPropertyValueStartRegEx/) {
+        # Note, a '-' property may be followed by a '+' property in the case of a "Modified"
+        # or "Name" property.  We only care about the ending value (i.e. the '+' property)
+        # in such circumstances.  So, we take the property value for the property to be its
+        # last parsed property value.
+        #
+        # FIXME: We may want to consider strictly enforcing a '-', '+' property ordering or
+        #        add error checking to prevent '+', '+', ..., '+' and other invalid combinations.
+        $propertyValueType = $1;
+        ($propertyValue, $_) = parseSvnPropertyValue($fileHandle, $_);
+        $_ = <$fileHandle> if defined($_) && /$svnPropertyValueNoNewlineRegEx/;
+    }
+
+    if (!$propertyValue) {
+        die("Failed to find the property value for the SVN property \"$propertyName\": \"$_\".");
+    }
+
+    my $propertyChangeDelta;
+    if ($propertyValueType eq "+" || $propertyValueType eq "Merged") {
+        $propertyChangeDelta = 1;
+    } elsif ($propertyValueType eq "-" || $propertyValueType eq "Reverse-merged") {
+        $propertyChangeDelta = -1;
+    } else {
+        die("Not reached.");
+    }
+
+    # We perform a simple validation that an "Added" or "Deleted" property
+    # change type corresponds with a "+" and "-" value type, respectively.
+    my $expectedChangeDelta;
+    if ($propertyChangeType eq "Added") {
+        $expectedChangeDelta = 1;
+    } elsif ($propertyChangeType eq "Deleted") {
+        $expectedChangeDelta = -1;
+    }
+
+    if ($expectedChangeDelta && $propertyChangeDelta != $expectedChangeDelta) {
+        die("The final property value type found \"$propertyValueType\" does not " .
+            "correspond to the property change type found \"$propertyChangeType\".");
+    }
+
+    my %propertyHash;
+    $propertyHash{name} = $propertyName;
+    $propertyHash{propertyChangeDelta} = $propertyChangeDelta;
+    $propertyHash{value} = $propertyValue;
+    return (\%propertyHash, $_);
+}
+
+# Parse the value of an SVN property from the given file handle, and advance
+# the handle so the last line read is the first line after the property value.
+#
+# This subroutine dies if the first line is an invalid SVN property value line
+# (i.e. a line that does not begin with "   +" or "   -").
+#
+# Args:
+#   $fileHandle: advanced so the last line read from the handle is the first
+#                line of the property value to parse.  This should be a line
+#                beginning with "   +" or "   -".
+#   $line: the line last read from $fileHandle.
+#
+# Returns ($propertyValue, $lastReadLine):
+#   $propertyValue: the value of the property.
+#   $lastReadLine: the line last read from $fileHandle.
+sub parseSvnPropertyValue($$)
+{
+    my ($fileHandle, $line) = @_;
+
+    $_ = $line;
+
+    my $propertyValue;
+    my $eol;
+    if (/$svnPropertyValueStartRegEx/) {
+        $propertyValue = $2; # Does not include the end-of-line character(s).
+        $eol = $POSTMATCH;
+    } else {
+        die("Failed to find property value beginning with '+', '-', 'Merged', or 'Reverse-merged': \"$_\".");
+    }
+
+    while (<$fileHandle>) {
+        if (/^[\r\n]+$/ || /$svnPropertyValueStartRegEx/ || /$svnPropertyStartRegEx/ || /$svnPropertyValueNoNewlineRegEx/) {
+            # Note, we may encounter an empty line before the contents of a binary patch.
+            # Also, we check for $svnPropertyValueStartRegEx because a '-' property may be
+            # followed by a '+' property in the case of a "Modified" or "Name" property.
+            # We check for $svnPropertyStartRegEx because it indicates the start of the
+            # next property to parse.
+            last;
+        }
+
+        # Temporarily strip off any end-of-line characters. We add the end-of-line characters
+        # from the previously processed line to the start of this line so that the last line
+        # of the property value does not end in end-of-line characters.
+        s/([\n\r]+)$//;
+        $propertyValue .= "$eol$_";
+        $eol = $1;
+    }
+
+    return ($propertyValue, $_);
+}
+
+# Parse a patch file created by svn-create-patch.
+#
+# Args:
+#   $fileHandle: A file handle to the patch file that has not yet been
+#                read from.
+#   $optionsHashRef: a hash reference representing optional options to use
+#                    when processing a diff.
+#     shouldNotUseIndexPathEOL: whether to use the line endings in the diff instead
+#                               instead of the line endings in the target file; the
+#                               value of 1 if svnConvertedText should use the line
+#                               endings in the diff.
+#
+# Returns:
+#   @diffHashRefs: an array of diff hash references.
+#                  See the %diffHash documentation above.
+sub parsePatch($;$)
+{
+    my ($fileHandle, $optionsHashRef) = @_;
+
+    my $newDiffHashRefs;
+    my @diffHashRefs; # return value
+
+    my $line = <$fileHandle>;
+
+    while (defined($line)) { # Otherwise, at EOF.
+
+        ($newDiffHashRefs, $line) = parseDiff($fileHandle, $line, $optionsHashRef);
+
+        push @diffHashRefs, @$newDiffHashRefs;
+    }
+
+    return @diffHashRefs;
+}
+
+# Prepare the results of parsePatch() for use in svn-apply and svn-unapply.
+#
+# Args:
+#   $shouldForce: Whether to continue processing if an unexpected
+#                 state occurs.
+#   @diffHashRefs: An array of references to %diffHashes.
+#                  See the %diffHash documentation above.
+#
+# Returns $preparedPatchHashRef:
+#   copyDiffHashRefs: A reference to an array of the $diffHashRefs in
+#                     @diffHashRefs that represent file copies. The original
+#                     ordering is preserved.
+#   nonCopyDiffHashRefs: A reference to an array of the $diffHashRefs in
+#                        @diffHashRefs that do not represent file copies.
+#                        The original ordering is preserved.
+#   sourceRevisionHash: A reference to a hash of source path to source
+#                       revision number.
+sub prepareParsedPatch($@)
+{
+    my ($shouldForce, @diffHashRefs) = @_;
+
+    my %copiedFiles;
+
+    # Return values
+    my @copyDiffHashRefs = ();
+    my @nonCopyDiffHashRefs = ();
+    my %sourceRevisionHash = ();
+    for my $diffHashRef (@diffHashRefs) {
+        my $copiedFromPath = $diffHashRef->{copiedFromPath};
+        my $indexPath = $diffHashRef->{indexPath};
+        my $sourceRevision = $diffHashRef->{sourceRevision};
+        my $sourcePath;
+
+        if (defined($copiedFromPath)) {
+            # Then the diff is a copy operation.
+            $sourcePath = $copiedFromPath;
+
+            # FIXME: Consider printing a warning or exiting if
+            #        exists($copiedFiles{$indexPath}) is true -- i.e. if
+            #        $indexPath appears twice as a copy target.
+            $copiedFiles{$indexPath} = $sourcePath;
+
+            push @copyDiffHashRefs, $diffHashRef;
+        } else {
+            # Then the diff is not a copy operation.
+            $sourcePath = $indexPath;
+
+            push @nonCopyDiffHashRefs, $diffHashRef;
+        }
+
+        if (defined($sourceRevision)) {
+            if (exists($sourceRevisionHash{$sourcePath}) &&
+                ($sourceRevisionHash{$sourcePath} != $sourceRevision)) {
+                if (!$shouldForce) {
+                    die "Two revisions of the same file required as a source:\n".
+                        "    $sourcePath:$sourceRevisionHash{$sourcePath}\n".
+                        "    $sourcePath:$sourceRevision";
+                }
+            }
+            $sourceRevisionHash{$sourcePath} = $sourceRevision;
+        }
+    }
+
+    my %preparedPatchHash;
+
+    $preparedPatchHash{copyDiffHashRefs} = \@copyDiffHashRefs;
+    $preparedPatchHash{nonCopyDiffHashRefs} = \@nonCopyDiffHashRefs;
+    $preparedPatchHash{sourceRevisionHash} = \%sourceRevisionHash;
+
+    return \%preparedPatchHash;
+}
+
+# Return localtime() for the project's time zone, given an integer time as
+# returned by Perl's time() function.
+sub localTimeInProjectTimeZone($)
+{
+    my $epochTime = shift;
+
+    # Change the time zone temporarily for the localtime() call.
+    my $savedTimeZone = $ENV{'TZ'};
+    $ENV{'TZ'} = $changeLogTimeZone;
+    my @localTime = localtime($epochTime);
+    if (defined $savedTimeZone) {
+         $ENV{'TZ'} = $savedTimeZone;
+    } else {
+         delete $ENV{'TZ'};
+    }
+
+    return @localTime;
+}
+
+# Set the reviewer and date in a ChangeLog patch, and return the new patch.
+#
+# Args:
+#   $patch: a ChangeLog patch as a string.
+#   $reviewer: the name of the reviewer, or undef if the reviewer should not be set.
+#   $epochTime: an integer time as returned by Perl's time() function.
+sub setChangeLogDateAndReviewer($$$)
+{
+    my ($patch, $reviewer, $epochTime) = @_;
+
+    my @localTime = localTimeInProjectTimeZone($epochTime);
+    my $newDate = strftime("%Y-%m-%d", @localTime);
+
+    my $firstChangeLogLineRegEx = qr#(\n\+)\d{4}-[^-]{2}-[^-]{2}(  )#;
+    $patch =~ s/$firstChangeLogLineRegEx/$1$newDate$2/;
+
+    if (defined($reviewer)) {
+        # We include a leading plus ("+") in the regular expression to make
+        # the regular expression less likely to match text in the leading junk
+        # for the patch, if the patch has leading junk.
+        $patch =~ s/(\n\+.*)NOBODY \(OOPS!\)/$1$reviewer/;
+    }
+
+    return $patch;
+}
+
+# If possible, returns a ChangeLog patch equivalent to the given one,
+# but with the newest ChangeLog entry inserted at the top of the
+# file -- i.e. no leading context and all lines starting with "+".
+#
+# If given a patch string not representable as a patch with the above
+# properties, it returns the input back unchanged.
+#
+# WARNING: This subroutine can return an inequivalent patch string if
+# both the beginning of the new ChangeLog file matches the beginning
+# of the source ChangeLog, and the source beginning was modified.
+# Otherwise, it is guaranteed to return an equivalent patch string,
+# if it returns.
+#
+# Applying this subroutine to ChangeLog patches allows svn-apply to
+# insert new ChangeLog entries at the top of the ChangeLog file.
+# svn-apply uses patch with --fuzz=3 to do this. We need to apply
+# this subroutine because the diff(1) command is greedy when matching
+# lines. A new ChangeLog entry with the same date and author as the
+# previous will match and cause the diff to have lines of starting
+# 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.              
+sub fixChangeLogPatch($)
+{
+    my $patch = shift; # $patch will only contain patch fragments for ChangeLog.
+
+    $patch =~ s|test_expectations.txt:|TestExpectations:|g;
+
+    $patch =~ /(\r?\n)/;
+    my $lineEnding = $1;
+    my @lines = split(/$lineEnding/, $patch);
+
+    my $i = 0; # We reuse the same index throughout.
+
+    # Skip to beginning of first chunk.
+    for (; $i < @lines; ++$i) {
+        if (substr($lines[$i], 0, 1) eq "@") {
+            last;
+        }
+    }
+    my $chunkStartIndex = ++$i;
+    my %changeLogHashRef;
+
+    # Optimization: do not process if new lines already begin the chunk.
+    if (substr($lines[$i], 0, 1) eq "+") {
+        $changeLogHashRef{patch} = $patch;
+        return \%changeLogHashRef;
+    }
+
+    # Skip to first line of newly added ChangeLog entry.
+    # For example, +2009-06-03  Eric Seidel  <eric@webkit.org>
+    my $dateStartRegEx = '^\+(\d{4}-\d{2}-\d{2})' # leading "+" and date
+                         . '\s+(.+)\s+' # name
+                         . '<([^<>]+)>$'; # e-mail address
+
+    for (; $i < @lines; ++$i) {
+        my $line = $lines[$i];
+        my $firstChar = substr($line, 0, 1);
+        if ($line =~ /$dateStartRegEx/) {
+            last;
+        } elsif ($firstChar eq " " or $firstChar eq "+") {
+            next;
+        }
+        $changeLogHashRef{patch} = $patch; # Do not change if, for example, "-" or "@" found.
+        return \%changeLogHashRef;
+    }
+    if ($i >= @lines) {
+        $changeLogHashRef{patch} = $patch; # Do not change if date not found.
+        return \%changeLogHashRef;
+    }
+    my $dateStartIndex = $i;
+
+    # Rewrite overlapping lines to lead with " ".
+    my @overlappingLines = (); # These will include a leading "+".
+    for (; $i < @lines; ++$i) {
+        my $line = $lines[$i];
+        if (substr($line, 0, 1) ne "+") {
+          last;
+        }
+        push(@overlappingLines, $line);
+        $lines[$i] = " " . substr($line, 1);
+    }
+
+    # Remove excess ending context, if necessary.
+    my $shouldTrimContext = 1;
+    for (; $i < @lines; ++$i) {
+        my $firstChar = substr($lines[$i], 0, 1);
+        if ($firstChar eq " ") {
+            next;
+        } elsif ($firstChar eq "@") {
+            last;
+        }
+        $shouldTrimContext = 0; # For example, if "+" or "-" encountered.
+        last;
+    }
+    my $deletedLineCount = 0;
+    if ($shouldTrimContext) { # Also occurs if end of file reached.
+        splice(@lines, $i - @overlappingLines, @overlappingLines);
+        $deletedLineCount = @overlappingLines;
+    }
+
+    # Work backwards, shifting overlapping lines towards front
+    # while checking that patch stays equivalent.
+    for ($i = $dateStartIndex - 1; @overlappingLines && $i >= $chunkStartIndex; --$i) {
+        my $line = $lines[$i];
+        if (substr($line, 0, 1) ne " ") {
+            next;
+        }
+        my $text = substr($line, 1);
+        my $newLine = pop(@overlappingLines);
+        if ($text ne substr($newLine, 1)) {
+            $changeLogHashRef{patch} = $patch; # Unexpected difference.
+            return \%changeLogHashRef;
+        }
+        $lines[$i] = "+$text";
+    }
+
+    # If @overlappingLines > 0, this is where we make use of the
+    # assumption that the beginning of the source file was not modified.
+    splice(@lines, $chunkStartIndex, 0, @overlappingLines);
+
+    # Update the date start index as it may have changed after shifting
+    # the overlapping lines towards the front.
+    for ($i = $chunkStartIndex; $i < $dateStartIndex; ++$i) {
+        $dateStartIndex = $i if $lines[$i] =~ /$dateStartRegEx/;
+    }
+    splice(@lines, $chunkStartIndex, $dateStartIndex - $chunkStartIndex); # Remove context of later entry.
+    $deletedLineCount += $dateStartIndex - $chunkStartIndex;
+
+    # Update the initial chunk range.
+    my $chunkRangeHashRef = parseChunkRange($lines[$chunkStartIndex - 1]);
+    if (!$chunkRangeHashRef) {
+        # 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.
+        $changeLogHashRef{patch} = $patch; # Error: unexpected patch string format.
+        return \%changeLogHashRef;
+    }
+    my $oldSourceLineCount = $chunkRangeHashRef->{lineCount};
+    my $oldTargetLineCount = $chunkRangeHashRef->{newLineCount};
+
+    my $sourceLineCount = $oldSourceLineCount + @overlappingLines - $deletedLineCount;
+    my $targetLineCount = $oldTargetLineCount + @overlappingLines - $deletedLineCount;
+    $lines[$chunkStartIndex - 1] = "@@ -1,$sourceLineCount +1,$targetLineCount @@";
+
+    $changeLogHashRef{patch} = join($lineEnding, @lines) . "\n"; # patch(1) expects an extra trailing newline.
+    return \%changeLogHashRef;
+}
+
+# This is a supporting method for runPatchCommand.
+#
+# Arg: the optional $args parameter passed to runPatchCommand (can be undefined).
+#
+# Returns ($patchCommand, $isForcing).
+#
+# This subroutine has unit tests in VCSUtils_unittest.pl.
+sub generatePatchCommand($)
+{
+    my ($passedArgsHashRef) = @_;
+
+    my $argsHashRef = { # Defaults
+        ensureForce => 0,
+        shouldReverse => 0,
+        options => []
+    };
+    
+    # Merges hash references. It's okay here if passed hash reference is undefined.
+    @{$argsHashRef}{keys %{$passedArgsHashRef}} = values %{$passedArgsHashRef};
+    
+    my $ensureForce = $argsHashRef->{ensureForce};
+    my $shouldReverse = $argsHashRef->{shouldReverse};
+    my $options = $argsHashRef->{options};
+
+    if (! $options) {
+        $options = [];
+    } else {
+        $options = [@{$options}]; # Copy to avoid side effects.
+    }
+
+    my $isForcing = 0;
+    if (grep /^--force$/, @{$options}) {
+        $isForcing = 1;
+    } elsif ($ensureForce) {
+        push @{$options}, "--force";
+        $isForcing = 1;
+    }
+
+    if ($shouldReverse) { # No check: --reverse should never be passed explicitly.
+        push @{$options}, "--reverse";
+    }
+
+    @{$options} = sort(@{$options}); # For easier testing.
+
+    my $patchCommand = join(" ", "patch -p0", @{$options});
+
+    return ($patchCommand, $isForcing);
+}
+
+# Apply the given patch using the patch(1) command.
+#
+# On success, return the resulting exit status. Otherwise, exit with the
+# exit status. If "--force" is passed as an option, however, then never
+# exit and always return the exit status.
+#
+# Args:
+#   $patch: a patch string.
+#   $repositoryRootPath: an absolute path to the repository root.
+#   $pathRelativeToRoot: the path of the file to be patched, relative to the
+#                        repository root. This should normally be the path
+#                        found in the patch's "Index:" line. It is passed
+#                        explicitly rather than reparsed from the patch
+#                        string for optimization purposes.
+#                            This is used only for error reporting. The
+#                        patch command gleans the actual file to patch
+#                        from the patch string.
+#   $args: a reference to a hash of optional arguments. The possible
+#          keys are --
+#            ensureForce: whether to ensure --force is passed (defaults to 0).
+#            shouldReverse: whether to pass --reverse (defaults to 0).
+#            options: a reference to an array of options to pass to the
+#                     patch command. The subroutine passes the -p0 option
+#                     no matter what. This should not include --reverse.
+#
+# This subroutine has unit tests in VCSUtils_unittest.pl.
+sub runPatchCommand($$$;$)
+{
+    my ($patch, $repositoryRootPath, $pathRelativeToRoot, $args) = @_;
+
+    my ($patchCommand, $isForcing) = generatePatchCommand($args);
+
+    # Temporarily change the working directory since the path found
+    # in the patch's "Index:" line is relative to the repository root
+    # (i.e. the same as $pathRelativeToRoot).
+    my $cwd = Cwd::getcwd();
+    chdir $repositoryRootPath;
+
+    open PATCH, "| $patchCommand" or die "Could not call \"$patchCommand\" for file \"$pathRelativeToRoot\": $!";
+    print PATCH $patch;
+    close PATCH;
+    my $exitStatus = exitStatus($?);
+
+    chdir $cwd;
+
+    if ($exitStatus && !$isForcing) {
+        print "Calling \"$patchCommand\" for file \"$pathRelativeToRoot\" returned " .
+              "status $exitStatus.  Pass --force to ignore patch failures.\n";
+        exit $exitStatus;
+    }
+
+    return $exitStatus;
+}
+
+# Merge ChangeLog patches using a three-file approach.
+#
+# This is used by resolve-ChangeLogs when it's operated as a merge driver
+# and when it's used to merge conflicts after a patch is applied or after
+# an svn update.
+#
+# It's also used for traditional rejected patches.
+#
+# Args:
+#   $fileMine:  The merged version of the file.  Also known in git as the
+#               other branch's version (%B) or "ours".
+#               For traditional patch rejects, this is the *.rej file.
+#   $fileOlder: The base version of the file.  Also known in git as the
+#               ancestor version (%O) or "base".
+#               For traditional patch rejects, this is the *.orig file.
+#   $fileNewer: The current version of the file.  Also known in git as the
+#               current version (%A) or "theirs".
+#               For traditional patch rejects, this is the original-named
+#               file.
+#
+# Returns 1 if merge was successful, else 0.
+sub mergeChangeLogs($$$)
+{
+    my ($fileMine, $fileOlder, $fileNewer) = @_;
+
+    my $traditionalReject = $fileMine =~ /\.rej$/ ? 1 : 0;
+
+    local $/ = undef;
+
+    my $patch;
+    if ($traditionalReject) {
+        open(DIFF, "<", $fileMine) or die $!;
+        $patch = <DIFF>;
+        close(DIFF);
+        rename($fileMine, "$fileMine.save");
+        rename($fileOlder, "$fileOlder.save");
+    } else {
+        open(DIFF, "diff -u -a --binary \"$fileOlder\" \"$fileMine\" |") or die $!;
+        $patch = <DIFF>;
+        close(DIFF);
+    }
+
+    unlink("${fileNewer}.orig");
+    unlink("${fileNewer}.rej");
+
+    open(PATCH, "| patch --force --fuzz=3 --binary \"$fileNewer\" > " . File::Spec->devnull()) or die $!;
+    if ($traditionalReject) {
+        print PATCH $patch;
+    } else {
+        my $changeLogHash = fixChangeLogPatch($patch);
+        print PATCH $changeLogHash->{patch};
+    }
+    close(PATCH);
+
+    my $result = !exitStatus($?);
+
+    # Refuse to merge the patch if it did not apply cleanly
+    if (-e "${fileNewer}.rej") {
+        unlink("${fileNewer}.rej");
+        if (-f "${fileNewer}.orig") {
+            unlink($fileNewer);
+            rename("${fileNewer}.orig", $fileNewer);
+        }
+    } else {
+        unlink("${fileNewer}.orig");
+    }
+
+    if ($traditionalReject) {
+        rename("$fileMine.save", $fileMine);
+        rename("$fileOlder.save", $fileOlder);
+    }
+
+    return $result;
+}
+
+sub gitConfig($)
+{
+    return unless $isGit;
+
+    my ($config) = @_;
+
+    my $result = `git config $config`;
+    if (($? >> 8)) {
+        $result = `git repo-config $config`;
+    }
+    chomp $result;
+    return $result;
+}
+
+sub changeLogSuffix()
+{
+    my $rootPath = determineVCSRoot();
+    my $changeLogSuffixFile = File::Spec->catfile($rootPath, ".changeLogSuffix");
+    return "" if ! -e $changeLogSuffixFile;
+    open FILE, $changeLogSuffixFile or die "Could not open $changeLogSuffixFile: $!";
+    my $changeLogSuffix = <FILE>;
+    chomp $changeLogSuffix;
+    close FILE;
+    return $changeLogSuffix;
+}
+
+sub changeLogFileName()
+{
+    return "ChangeLog" . changeLogSuffix()
+}
+
+sub changeLogNameError($)
+{
+    my ($message) = @_;
+    print STDERR "$message\nEither:\n";
+    print STDERR "  set CHANGE_LOG_NAME in your environment\n";
+    print STDERR "  OR pass --name= on the command line\n";
+    print STDERR "  OR set REAL_NAME in your environment";
+    print STDERR "  OR git users can set 'git config user.name'\n";
+    exit(1);
+}
+
+sub changeLogName()
+{
+    my $name = $ENV{CHANGE_LOG_NAME} || $ENV{REAL_NAME} || gitConfig("user.name") || (split /\s*,\s*/, (getpwuid $<)[6])[0];
+
+    changeLogNameError("Failed to determine ChangeLog name.") unless $name;
+    # getpwuid seems to always succeed on windows, returning the username instead of the full name.  This check will catch that case.
+    changeLogNameError("'$name' does not contain a space!  ChangeLogs should contain your full name.") unless ($name =~ /\S\s\S/);
+
+    return $name;
+}
+
+sub changeLogEmailAddressError($)
+{
+    my ($message) = @_;
+    print STDERR "$message\nEither:\n";
+    print STDERR "  set CHANGE_LOG_EMAIL_ADDRESS in your environment\n";
+    print STDERR "  OR pass --email= on the command line\n";
+    print STDERR "  OR set EMAIL_ADDRESS in your environment\n";
+    print STDERR "  OR git users can set 'git config user.email'\n";
+    exit(1);
+}
+
+sub changeLogEmailAddress()
+{
+    my $emailAddress = $ENV{CHANGE_LOG_EMAIL_ADDRESS} || $ENV{EMAIL_ADDRESS} || gitConfig("user.email");
+
+    changeLogEmailAddressError("Failed to determine email address for ChangeLog.") unless $emailAddress;
+    changeLogEmailAddressError("Email address '$emailAddress' does not contain '\@' and is likely invalid.") unless ($emailAddress =~ /\@/);
+
+    return $emailAddress;
+}
+
+# http://tools.ietf.org/html/rfc1924
+sub decodeBase85($)
+{
+    my ($encoded) = @_;
+    my %table;
+    my @characters = ('0'..'9', 'A'..'Z', 'a'..'z', '!', '#', '$', '%', '&', '(', ')', '*', '+', '-', ';', '<', '=', '>', '?', '@', '^', '_', '`', '{', '|', '}', '~');
+    for (my $i = 0; $i < 85; $i++) {
+        $table{$characters[$i]} = $i;
+    }
+
+    my $decoded = '';
+    my @encodedChars = $encoded =~ /./g;
+
+    for (my $encodedIter = 0; defined($encodedChars[$encodedIter]);) {
+        my $digit = 0;
+        for (my $i = 0; $i < 5; $i++) {
+            $digit *= 85;
+            my $char = $encodedChars[$encodedIter];
+            $digit += $table{$char};
+            $encodedIter++;
+        }
+
+        for (my $i = 0; $i < 4; $i++) {
+            $decoded .= chr(($digit >> (3 - $i) * 8) & 255);
+        }
+    }
+
+    return $decoded;
+}
+
+sub decodeGitBinaryChunk($$)
+{
+    my ($contents, $fullPath) = @_;
+
+    # Load this module lazily in case the user don't have this module
+    # and won't handle git binary patches.
+    require Compress::Zlib;
+
+    my $encoded = "";
+    my $compressedSize = 0;
+    while ($contents =~ /^([A-Za-z])(.*)$/gm) {
+        my $line = $2;
+        next if $line eq "";
+        die "$fullPath: unexpected size of a line: $&" if length($2) % 5 != 0;
+        my $actualSize = length($2) / 5 * 4;
+        my $encodedExpectedSize = ord($1);
+        my $expectedSize = $encodedExpectedSize <= ord("Z") ? $encodedExpectedSize - ord("A") + 1 : $encodedExpectedSize - ord("a") + 27;
+
+        die "$fullPath: unexpected size of a line: $&" if int(($expectedSize + 3) / 4) * 4 != $actualSize;
+        $compressedSize += $expectedSize;
+        $encoded .= $line;
+    }
+
+    my $compressed = decodeBase85($encoded);
+    $compressed = substr($compressed, 0, $compressedSize);
+    return Compress::Zlib::uncompress($compressed);
+}
+
+sub decodeGitBinaryPatch($$)
+{
+    my ($contents, $fullPath) = @_;
+
+    # Git binary patch has two chunks. One is for the normal patching
+    # and another is for the reverse patching.
+    #
+    # Each chunk a line which starts from either "literal" or "delta",
+    # followed by a number which specifies decoded size of the chunk.
+    #
+    # Then, content of the chunk comes. To decode the content, we
+    # need decode it with base85 first, and then zlib.
+    my $gitPatchRegExp = '(literal|delta) ([0-9]+)\n([A-Za-z0-9!#$%&()*+-;<=>?@^_`{|}~\\n]*?)\n\n';
+    if ($contents !~ m"\nGIT binary patch\n$gitPatchRegExp$gitPatchRegExp\Z") {
+        die "$fullPath: unknown git binary patch format"
+    }
+
+    my $binaryChunkType = $1;
+    my $binaryChunkExpectedSize = $2;
+    my $encodedChunk = $3;
+    my $reverseBinaryChunkType = $4;
+    my $reverseBinaryChunkExpectedSize = $5;
+    my $encodedReverseChunk = $6;
+
+    my $binaryChunk = decodeGitBinaryChunk($encodedChunk, $fullPath);
+    my $binaryChunkActualSize = length($binaryChunk);
+    my $reverseBinaryChunk = decodeGitBinaryChunk($encodedReverseChunk, $fullPath);
+    my $reverseBinaryChunkActualSize = length($reverseBinaryChunk);
+
+    die "$fullPath: unexpected size of the first chunk (expected $binaryChunkExpectedSize but was $binaryChunkActualSize" if ($binaryChunkType eq "literal" and $binaryChunkExpectedSize != $binaryChunkActualSize);
+    die "$fullPath: unexpected size of the second chunk (expected $reverseBinaryChunkExpectedSize but was $reverseBinaryChunkActualSize" if ($reverseBinaryChunkType eq "literal" and $reverseBinaryChunkExpectedSize != $reverseBinaryChunkActualSize);
+
+    return ($binaryChunkType, $binaryChunk, $reverseBinaryChunkType, $reverseBinaryChunk);
+}
+
+sub readByte($$)
+{
+    my ($data, $location) = @_;
+    
+    # Return the byte at $location in $data as a numeric value. 
+    return ord(substr($data, $location, 1));
+}
+
+# The git binary delta format is undocumented, except in code:
+# - https://github.com/git/git/blob/master/delta.h:get_delta_hdr_size is the source
+#   of the algorithm in decodeGitBinaryPatchDeltaSize.
+# - https://github.com/git/git/blob/master/patch-delta.c:patch_delta is the source
+#   of the algorithm in applyGitBinaryPatchDelta.
+sub decodeGitBinaryPatchDeltaSize($)
+{
+    my ($binaryChunk) = @_;
+    
+    # Source and destination buffer sizes are stored in 7-bit chunks at the
+    # start of the binary delta patch data.  The highest bit in each byte
+    # except the last is set; the remaining 7 bits provide the next
+    # chunk of the size.  The chunks are stored in ascending significance
+    # order.
+    my $cmd;
+    my $size = 0;
+    my $shift = 0;
+    for (my $i = 0; $i < length($binaryChunk);) {
+        $cmd = readByte($binaryChunk, $i++);
+        $size |= ($cmd & 0x7f) << $shift;
+        $shift += 7;
+        if (!($cmd & 0x80)) {
+            return ($size, $i);
+        }
+    }
+}
+
+sub applyGitBinaryPatchDelta($$)
+{
+    my ($binaryChunk, $originalContents) = @_;
+    
+    # Git delta format consists of two headers indicating source buffer size
+    # and result size, then a series of commands.  Each command is either
+    # a copy-from-old-version (the 0x80 bit is set) or a copy-from-delta
+    # command.  Commands are applied sequentially to generate the result.
+    #
+    # A copy-from-old-version command encodes an offset and size to copy
+    # from in subsequent bits, while a copy-from-delta command consists only
+    # of the number of bytes to copy from the delta.
+
+    # We don't use these values, but we need to know how big they are so that
+    # we can skip to the diff data.
+    my ($size, $bytesUsed) = decodeGitBinaryPatchDeltaSize($binaryChunk);
+    $binaryChunk = substr($binaryChunk, $bytesUsed);
+    ($size, $bytesUsed) = decodeGitBinaryPatchDeltaSize($binaryChunk);
+    $binaryChunk = substr($binaryChunk, $bytesUsed);
+
+    my $out = "";
+    for (my $i = 0; $i < length($binaryChunk); ) {
+        my $cmd = ord(substr($binaryChunk, $i++, 1));
+        if ($cmd & 0x80) {
+            # Extract an offset and size from the delta data, then copy
+            # $size bytes from $offset in the original data into the output.
+            my $offset = 0;
+            my $size = 0;
+            if ($cmd & 0x01) { $offset = readByte($binaryChunk, $i++); }
+            if ($cmd & 0x02) { $offset |= readByte($binaryChunk, $i++) << 8; }
+            if ($cmd & 0x04) { $offset |= readByte($binaryChunk, $i++) << 16; }
+            if ($cmd & 0x08) { $offset |= readByte($binaryChunk, $i++) << 24; }
+            if ($cmd & 0x10) { $size = readByte($binaryChunk, $i++); }
+            if ($cmd & 0x20) { $size |= readByte($binaryChunk, $i++) << 8; }
+            if ($cmd & 0x40) { $size |= readByte($binaryChunk, $i++) << 16; }
+            if ($size == 0) { $size = 0x10000; }
+            $out .= substr($originalContents, $offset, $size);
+        } elsif ($cmd) {
+            # Copy $cmd bytes from the delta data into the output.
+            $out .= substr($binaryChunk, $i, $cmd);
+            $i += $cmd;
+        } else {
+            die "unexpected delta opcode 0";
+        }
+    }
+
+    return $out;
+}
+
+sub escapeSubversionPath($)
+{
+    my ($path) = @_;
+    $path .= "@" if $path =~ /@/;
+    return $path;
+}
+
+sub runCommand(@)
+{
+    my @args = @_;
+    my $pid = open(CHILD, "-|");
+    if (!defined($pid)) {
+        die "Failed to fork(): $!";
+    }
+    if ($pid) {
+        # Parent process
+        my $childStdout;
+        while (<CHILD>) {
+            $childStdout .= $_;
+        }
+        close(CHILD);
+        my %childOutput;
+        $childOutput{exitStatus} = exitStatus($?);
+        $childOutput{stdout} = $childStdout if $childStdout;
+        return \%childOutput;
+    }
+    # Child process
+    # FIXME: Consider further hardening of this function, including sanitizing the environment.
+    exec { $args[0] } @args or die "Failed to exec(): $!";
+}
+
+sub gitCommitForSVNRevision
+{
+    my ($svnRevision) = @_;
+    my $command = "git svn find-rev r" . $svnRevision;
+    $command = "LC_ALL=C $command" if !isWindows();
+    my $gitHash = `$command`;
+    if (!defined($gitHash)) {
+        $gitHash = "unknown";
+        warn "Unable to determine GIT commit from SVN revision";
+    } else {
+        chop($gitHash);
+    }
+    return $gitHash;
+}
+
+sub listOfChangedFilesBetweenRevisions
+{
+    my ($sourceDir, $firstRevision, $lastRevision) = @_;
+    my $command;
+
+    if ($firstRevision eq "unknown" or $lastRevision eq "unknown") {
+        return ();
+    }
+
+    # Some VCS functions don't work from within the build dir, so always
+    # go to the source dir first.
+    my $cwd = Cwd::getcwd();
+    chdir $sourceDir;
+
+    if (isGit()) {
+        my $firstCommit = gitCommitForSVNRevision($firstRevision);
+        my $lastCommit = gitCommitForSVNRevision($lastRevision);
+        $command = "git diff --name-status $firstCommit..$lastCommit";
+    } elsif (isSVN()) {
+        $command = "svn diff --summarize -r $firstRevision:$lastRevision";
+    }
+
+    my @result = ();
+
+    if ($command) {
+        my $diffOutput = `$command`;
+        $diffOutput =~ s/^[A-Z]\s+//gm;
+        @result = split(/[\r\n]+/, $diffOutput);
+    }
+
+    chdir $cwd;
+
+    return @result;
+}
+
+
+1;
diff --git a/Tools/Scripts/add-include b/Tools/Scripts/add-include
new file mode 100755
index 0000000..d0525eb
--- /dev/null
+++ b/Tools/Scripts/add-include
@@ -0,0 +1,135 @@
+#!/usr/bin/perl -w
+
+# Copyright 2009 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. ``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
+# 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. 
+
+# Helper script to add includes to source files.
+
+use strict;
+
+my $headerPattern = '[\"<][A-Za-z][A-Za-z0-9_/]+(\.h)?[\">]'; # " Make Xcode formatter happy.
+
+my $headerToAdd = shift @ARGV or die;
+$headerToAdd =~ /^([A-Za-z][A-Za-z0-9]+)\.h$/ or die "Header to add must be a .h file: $headerToAdd.\n";
+
+sub includesParagraph;
+
+FILE: for my $filename (@ARGV) {
+    unless ($filename =~ /(\w+)\.cpp$/) { print STDERR "Command line args must be .cpp files: $filename.\n"; next FILE; }
+
+    my $base = $1;
+
+    my $sawConfig = 0;
+    my $sawSelfInclude = 0;
+
+    my $pastIncludes = 0;
+    my %includes;
+
+    my $beforeIncludes = "";
+    my $afterIncludes = "";
+
+    my $currentCondition = "";
+
+    my $entireFileCondition = "";
+
+    unless (open INPUT, "<", $filename) { print STDERR "File does not exist: $filename\n"; next FILE; }
+    while (my $line = <INPUT>) {
+        if ($line =~ /^\s*#(include|import)\s*($headerPattern)\s*\n/) {
+            my $include = $2;
+            if ($pastIncludes) { print STDERR "Saw more includes after include section in $filename, line $.\n"; next FILE; }
+            if ($include eq "\"config.h\"") {
+                $sawConfig = 1;
+            } else {
+                unless ($sawConfig) { print STDERR "First include must be config.h in $filename, line $.\n"; next FILE; }
+                if ($include eq "\"$base.h\"") {
+                    $sawSelfInclude = 1;
+                } else {
+                    unless ($sawSelfInclude) { print STDERR "Second include must be $base.h in $filename, line $.\n"; next FILE; }
+                    $includes{$currentCondition}{$include} = 1;
+                }
+            }
+        } else {
+            if ($sawConfig && !$pastIncludes) {
+                if ($line =~ /^\s*#\s*if\s+(.+?)\s*$/) {
+                    my $condition = $1;
+                    if (!$sawSelfInclude) {
+                        $entireFileCondition = $1;
+                        next;
+                    }
+                    unless ($currentCondition eq "") { print STDERR "Nested #if in include section in $filename, line $.\n"; next FILE; }
+                    $currentCondition = $condition;
+                    next;
+                }
+                if ($line =~ /^\s*#\s*endif\s*$/) {
+                    unless ($currentCondition ne "") { print STDERR "Extra #endif in include section in $filename, line $.\n"; next FILE; }
+                    $currentCondition = "";
+                    next;
+                }
+            }
+            if (!$sawConfig) {
+                $beforeIncludes .= $line;
+            } else {
+                $pastIncludes = 1 if $line !~ /^\s*$/;
+                if ($pastIncludes) {
+                    unless ($currentCondition eq "") { print STDERR "Unterminated #if in include section in $filename, line $.\n"; next FILE; }
+                    $afterIncludes .= $line;
+                }
+            }
+        }
+    }
+    close INPUT or die;
+
+    $includes{""}{"\"$headerToAdd\""} = 1;
+
+    $beforeIncludes =~ s/\n+$//;
+    $afterIncludes =~ s/^\n+//;
+
+    my $contents = $beforeIncludes;
+    $contents .= "\n\n#include \"config.h\"\n";
+    $contents .= "\n#if $entireFileCondition\n" if $entireFileCondition ne "";
+    $contents .= "#include \"$base.h\"\n\n";
+    for my $condition (sort keys %includes) {
+        $contents .= "#if $condition\n" unless $condition eq "";
+        $contents .= includesParagraph($includes{$condition});
+        $contents .= "#endif\n" unless $condition eq "";
+        $contents .= "\n";
+    }
+    $contents .= $afterIncludes;
+
+    unless (open OUTPUT, ">", $filename) { print STDERR "Could not open file for writing: $filename\n"; next FILE; };
+    print OUTPUT $contents;
+    close OUTPUT or die;
+}
+
+sub includesParagraph()
+{
+    my ($includes) = @_;
+
+    my $paragraph = "";
+
+    for my $include (sort keys %{$includes}) {
+        $paragraph .= "#include $include\n";
+    }
+
+    return $paragraph;
+}
diff --git a/Tools/Scripts/bencher b/Tools/Scripts/bencher
new file mode 100755
index 0000000..0b44da1
--- /dev/null
+++ b/Tools/Scripts/bencher
@@ -0,0 +1,2098 @@
+#!/usr/bin/env ruby
+
+# Copyright (C) 2011 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.
+
+require 'rubygems'
+
+require 'getoptlong'
+require 'pathname'
+require 'tempfile'
+require 'socket'
+
+begin
+  require 'json'
+rescue LoadError => e
+  $stderr.puts "It does not appear that you have the 'json' package installed.  Try running 'sudo gem install json'."
+  exit 1
+end
+
+# Configuration
+
+CONFIGURATION_FLNM = ENV["HOME"]+"/.bencher"
+
+unless FileTest.exist? CONFIGURATION_FLNM
+  $stderr.puts "Error: no configuration file at ~/.bencher."
+  $stderr.puts "This file should contain paths to SunSpider, V8, and Kraken, as well as a"
+  $stderr.puts "temporary directory that bencher can use for its remote mode. It should be"
+  $stderr.puts "formatted in JSON.  For example:"
+  $stderr.puts "{"
+  $stderr.puts "    \"sunSpiderPath\": \"/Volumes/Data/pizlo/OpenSource/PerformanceTests/SunSpider/tests/sunspider-1.0\","
+  $stderr.puts "    \"v8Path\": \"/Volumes/Data/pizlo/OpenSource/PerformanceTests/SunSpider/tests/v8-v6\","
+  $stderr.puts "    \"krakenPath\": \"/Volumes/Data/pizlo/kraken/kraken-e119421cb325/tests/kraken-1.1\","
+  $stderr.puts "    \"tempPath\": \"/Volumes/Data/pizlo/bencher/temp\""
+  $stderr.puts "}"
+  exit 1
+end
+
+CONFIGURATION = JSON.parse(File::read(CONFIGURATION_FLNM))
+
+SUNSPIDER_PATH = CONFIGURATION["sunSpiderPath"]
+V8_PATH = CONFIGURATION["v8Path"]
+KRAKEN_PATH = CONFIGURATION["krakenPath"]
+TEMP_PATH = CONFIGURATION["tempPath"]
+BENCH_DATA_PATH = TEMP_PATH + "/benchdata"
+
+IBR_LOOKUP=[0.00615583, 0.0975, 0.22852, 0.341628, 0.430741, 0.500526, 0.555933, 
+            0.600706, 0.637513, 0.668244, 0.694254, 0.716537, 0.735827, 0.752684, 
+            0.767535, 0.780716, 0.792492, 0.803074, 0.812634, 0.821313, 0.829227, 
+            0.836472, 0.843129, 0.849267, 0.854943, 0.860209, 0.865107, 0.869674, 
+            0.873942, 0.877941, 0.881693, 0.885223, 0.888548, 0.891686, 0.894652, 
+            0.897461, 0.900124, 0.902652, 0.905056, 0.907343, 0.909524, 0.911604, 
+            0.91359, 0.91549, 0.917308, 0.919049, 0.920718, 0.92232, 0.923859, 0.925338, 
+            0.926761, 0.92813, 0.929449, 0.930721, 0.931948, 0.933132, 0.934275, 0.93538, 
+            0.936449, 0.937483, 0.938483, 0.939452, 0.940392, 0.941302, 0.942185, 
+            0.943042, 0.943874, 0.944682, 0.945467, 0.94623, 0.946972, 0.947694, 
+            0.948396, 0.94908, 0.949746, 0.950395, 0.951027, 0.951643, 0.952244, 
+            0.952831, 0.953403, 0.953961, 0.954506, 0.955039, 0.955559, 0.956067, 
+            0.956563, 0.957049, 0.957524, 0.957988, 0.958443, 0.958887, 0.959323, 
+            0.959749, 0.960166, 0.960575, 0.960975, 0.961368, 0.961752, 0.962129, 
+            0.962499, 0.962861, 0.963217, 0.963566, 0.963908, 0.964244, 0.964574, 
+            0.964897, 0.965215, 0.965527, 0.965834, 0.966135, 0.966431, 0.966722, 
+            0.967007, 0.967288, 0.967564, 0.967836, 0.968103, 0.968366, 0.968624, 
+            0.968878, 0.969128, 0.969374, 0.969617, 0.969855, 0.97009, 0.970321, 
+            0.970548, 0.970772, 0.970993, 0.97121, 0.971425, 0.971636, 0.971843, 
+            0.972048, 0.97225, 0.972449, 0.972645, 0.972839, 0.973029, 0.973217, 
+            0.973403, 0.973586, 0.973766, 0.973944, 0.97412, 0.974293, 0.974464, 
+            0.974632, 0.974799, 0.974963, 0.975125, 0.975285, 0.975443, 0.975599, 
+            0.975753, 0.975905, 0.976055, 0.976204, 0.97635, 0.976495, 0.976638, 
+            0.976779, 0.976918, 0.977056, 0.977193, 0.977327, 0.97746, 0.977592, 
+            0.977722, 0.97785, 0.977977, 0.978103, 0.978227, 0.978349, 0.978471, 
+            0.978591, 0.978709, 0.978827, 0.978943, 0.979058, 0.979171, 0.979283, 
+            0.979395, 0.979504, 0.979613, 0.979721, 0.979827, 0.979933, 0.980037, 
+            0.98014, 0.980242, 0.980343, 0.980443, 0.980543, 0.980641, 0.980738, 
+            0.980834, 0.980929, 0.981023, 0.981116, 0.981209, 0.9813, 0.981391, 0.981481, 
+            0.981569, 0.981657, 0.981745, 0.981831, 0.981916, 0.982001, 0.982085, 
+            0.982168, 0.982251, 0.982332, 0.982413, 0.982493, 0.982573, 0.982651, 
+            0.982729, 0.982807, 0.982883, 0.982959, 0.983034, 0.983109, 0.983183, 
+            0.983256, 0.983329, 0.983401, 0.983472, 0.983543, 0.983613, 0.983683, 
+            0.983752, 0.98382, 0.983888, 0.983956, 0.984022, 0.984089, 0.984154, 
+            0.984219, 0.984284, 0.984348, 0.984411, 0.984474, 0.984537, 0.984599, 
+            0.98466, 0.984721, 0.984782, 0.984842, 0.984902, 0.984961, 0.985019, 
+            0.985077, 0.985135, 0.985193, 0.985249, 0.985306, 0.985362, 0.985417, 
+            0.985472, 0.985527, 0.985582, 0.985635, 0.985689, 0.985742, 0.985795, 
+            0.985847, 0.985899, 0.985951, 0.986002, 0.986053, 0.986103, 0.986153, 
+            0.986203, 0.986252, 0.986301, 0.98635, 0.986398, 0.986446, 0.986494, 
+            0.986541, 0.986588, 0.986635, 0.986681, 0.986727, 0.986773, 0.986818, 
+            0.986863, 0.986908, 0.986953, 0.986997, 0.987041, 0.987084, 0.987128, 
+            0.987171, 0.987213, 0.987256, 0.987298, 0.98734, 0.987381, 0.987423, 
+            0.987464, 0.987504, 0.987545, 0.987585, 0.987625, 0.987665, 0.987704, 
+            0.987744, 0.987783, 0.987821, 0.98786, 0.987898, 0.987936, 0.987974, 
+            0.988011, 0.988049, 0.988086, 0.988123, 0.988159, 0.988196, 0.988232, 
+            0.988268, 0.988303, 0.988339, 0.988374, 0.988409, 0.988444, 0.988479, 
+            0.988513, 0.988547, 0.988582, 0.988615, 0.988649, 0.988682, 0.988716, 
+            0.988749, 0.988782, 0.988814, 0.988847, 0.988879, 0.988911, 0.988943, 
+            0.988975, 0.989006, 0.989038, 0.989069, 0.9891, 0.989131, 0.989161, 0.989192, 
+            0.989222, 0.989252, 0.989282, 0.989312, 0.989342, 0.989371, 0.989401, 
+            0.98943, 0.989459, 0.989488, 0.989516, 0.989545, 0.989573, 0.989602, 0.98963, 
+            0.989658, 0.989685, 0.989713, 0.98974, 0.989768, 0.989795, 0.989822, 
+            0.989849, 0.989876, 0.989902, 0.989929, 0.989955, 0.989981, 0.990007, 
+            0.990033, 0.990059, 0.990085, 0.99011, 0.990136, 0.990161, 0.990186, 
+            0.990211, 0.990236, 0.990261, 0.990285, 0.99031, 0.990334, 0.990358, 
+            0.990383, 0.990407, 0.99043, 0.990454, 0.990478, 0.990501, 0.990525, 
+            0.990548, 0.990571, 0.990594, 0.990617, 0.99064, 0.990663, 0.990686, 
+            0.990708, 0.990731, 0.990753, 0.990775, 0.990797, 0.990819, 0.990841, 
+            0.990863, 0.990885, 0.990906, 0.990928, 0.990949, 0.99097, 0.990991, 
+            0.991013, 0.991034, 0.991054, 0.991075, 0.991096, 0.991116, 0.991137, 
+            0.991157, 0.991178, 0.991198, 0.991218, 0.991238, 0.991258, 0.991278, 
+            0.991298, 0.991317, 0.991337, 0.991356, 0.991376, 0.991395, 0.991414, 
+            0.991433, 0.991452, 0.991471, 0.99149, 0.991509, 0.991528, 0.991547, 
+            0.991565, 0.991584, 0.991602, 0.99162, 0.991639, 0.991657, 0.991675, 
+            0.991693, 0.991711, 0.991729, 0.991746, 0.991764, 0.991782, 0.991799, 
+            0.991817, 0.991834, 0.991851, 0.991869, 0.991886, 0.991903, 0.99192, 
+            0.991937, 0.991954, 0.991971, 0.991987, 0.992004, 0.992021, 0.992037, 
+            0.992054, 0.99207, 0.992086, 0.992103, 0.992119, 0.992135, 0.992151, 
+            0.992167, 0.992183, 0.992199, 0.992215, 0.99223, 0.992246, 0.992262, 
+            0.992277, 0.992293, 0.992308, 0.992324, 0.992339, 0.992354, 0.992369, 
+            0.992384, 0.9924, 0.992415, 0.992429, 0.992444, 0.992459, 0.992474, 0.992489, 
+            0.992503, 0.992518, 0.992533, 0.992547, 0.992561, 0.992576, 0.99259, 
+            0.992604, 0.992619, 0.992633, 0.992647, 0.992661, 0.992675, 0.992689, 
+            0.992703, 0.992717, 0.99273, 0.992744, 0.992758, 0.992771, 0.992785, 
+            0.992798, 0.992812, 0.992825, 0.992839, 0.992852, 0.992865, 0.992879, 
+            0.992892, 0.992905, 0.992918, 0.992931, 0.992944, 0.992957, 0.99297, 
+            0.992983, 0.992995, 0.993008, 0.993021, 0.993034, 0.993046, 0.993059, 
+            0.993071, 0.993084, 0.993096, 0.993109, 0.993121, 0.993133, 0.993145, 
+            0.993158, 0.99317, 0.993182, 0.993194, 0.993206, 0.993218, 0.99323, 0.993242, 
+            0.993254, 0.993266, 0.993277, 0.993289, 0.993301, 0.993312, 0.993324, 
+            0.993336, 0.993347, 0.993359, 0.99337, 0.993382, 0.993393, 0.993404, 
+            0.993416, 0.993427, 0.993438, 0.993449, 0.99346, 0.993472, 0.993483, 
+            0.993494, 0.993505, 0.993516, 0.993527, 0.993538, 0.993548, 0.993559, 
+            0.99357, 0.993581, 0.993591, 0.993602, 0.993613, 0.993623, 0.993634, 
+            0.993644, 0.993655, 0.993665, 0.993676, 0.993686, 0.993697, 0.993707, 
+            0.993717, 0.993727, 0.993738, 0.993748, 0.993758, 0.993768, 0.993778, 
+            0.993788, 0.993798, 0.993808, 0.993818, 0.993828, 0.993838, 0.993848, 
+            0.993858, 0.993868, 0.993877, 0.993887, 0.993897, 0.993907, 0.993916, 
+            0.993926, 0.993935, 0.993945, 0.993954, 0.993964, 0.993973, 0.993983, 
+            0.993992, 0.994002, 0.994011, 0.99402, 0.99403, 0.994039, 0.994048, 0.994057, 
+            0.994067, 0.994076, 0.994085, 0.994094, 0.994103, 0.994112, 0.994121, 
+            0.99413, 0.994139, 0.994148, 0.994157, 0.994166, 0.994175, 0.994183, 
+            0.994192, 0.994201, 0.99421, 0.994218, 0.994227, 0.994236, 0.994244, 
+            0.994253, 0.994262, 0.99427, 0.994279, 0.994287, 0.994296, 0.994304, 
+            0.994313, 0.994321, 0.994329, 0.994338, 0.994346, 0.994354, 0.994363, 
+            0.994371, 0.994379, 0.994387, 0.994395, 0.994404, 0.994412, 0.99442, 
+            0.994428, 0.994436, 0.994444, 0.994452, 0.99446, 0.994468, 0.994476, 
+            0.994484, 0.994492, 0.9945, 0.994508, 0.994516, 0.994523, 0.994531, 0.994539, 
+            0.994547, 0.994554, 0.994562, 0.99457, 0.994577, 0.994585, 0.994593, 0.9946, 
+            0.994608, 0.994615, 0.994623, 0.994631, 0.994638, 0.994645, 0.994653, 
+            0.99466, 0.994668, 0.994675, 0.994683, 0.99469, 0.994697, 0.994705, 0.994712, 
+            0.994719, 0.994726, 0.994734, 0.994741, 0.994748, 0.994755, 0.994762, 
+            0.994769, 0.994777, 0.994784, 0.994791, 0.994798, 0.994805, 0.994812, 
+            0.994819, 0.994826, 0.994833, 0.99484, 0.994847, 0.994854, 0.99486, 0.994867, 
+            0.994874, 0.994881, 0.994888, 0.994895, 0.994901, 0.994908, 0.994915, 
+            0.994922, 0.994928, 0.994935, 0.994942, 0.994948, 0.994955, 0.994962, 
+            0.994968, 0.994975, 0.994981, 0.994988, 0.994994, 0.995001, 0.995007, 
+            0.995014, 0.99502, 0.995027, 0.995033, 0.99504, 0.995046, 0.995052, 0.995059, 
+            0.995065, 0.995071, 0.995078, 0.995084, 0.99509, 0.995097, 0.995103, 
+            0.995109, 0.995115, 0.995121, 0.995128, 0.995134, 0.99514, 0.995146, 
+            0.995152, 0.995158, 0.995164, 0.995171, 0.995177, 0.995183, 0.995189, 
+            0.995195, 0.995201, 0.995207, 0.995213, 0.995219, 0.995225, 0.995231, 
+            0.995236, 0.995242, 0.995248, 0.995254, 0.99526, 0.995266, 0.995272, 
+            0.995277, 0.995283, 0.995289, 0.995295, 0.995301, 0.995306, 0.995312, 
+            0.995318, 0.995323, 0.995329, 0.995335, 0.99534, 0.995346, 0.995352, 
+            0.995357, 0.995363, 0.995369, 0.995374, 0.99538, 0.995385, 0.995391, 
+            0.995396, 0.995402, 0.995407, 0.995413, 0.995418, 0.995424, 0.995429, 
+            0.995435, 0.99544, 0.995445, 0.995451, 0.995456, 0.995462, 0.995467, 
+            0.995472, 0.995478, 0.995483, 0.995488, 0.995493, 0.995499, 0.995504, 
+            0.995509, 0.995515, 0.99552, 0.995525, 0.99553, 0.995535, 0.995541, 0.995546, 
+            0.995551, 0.995556, 0.995561, 0.995566, 0.995571, 0.995577, 0.995582, 
+            0.995587, 0.995592, 0.995597, 0.995602, 0.995607, 0.995612, 0.995617, 
+            0.995622, 0.995627, 0.995632, 0.995637, 0.995642, 0.995647, 0.995652, 
+            0.995657, 0.995661, 0.995666, 0.995671, 0.995676, 0.995681, 0.995686, 
+            0.995691, 0.995695, 0.9957, 0.995705, 0.99571, 0.995715, 0.995719, 0.995724, 
+            0.995729, 0.995734, 0.995738, 0.995743, 0.995748, 0.995753, 0.995757, 
+            0.995762, 0.995767, 0.995771, 0.995776, 0.995781, 0.995785, 0.99579, 
+            0.995794, 0.995799, 0.995804, 0.995808, 0.995813, 0.995817, 0.995822, 
+            0.995826, 0.995831, 0.995835, 0.99584, 0.995844, 0.995849, 0.995853, 
+            0.995858, 0.995862, 0.995867, 0.995871, 0.995876, 0.99588, 0.995885, 
+            0.995889, 0.995893, 0.995898, 0.995902, 0.995906, 0.995911, 0.995915, 
+            0.99592, 0.995924, 0.995928, 0.995932, 0.995937, 0.995941, 0.995945, 0.99595, 
+            0.995954, 0.995958, 0.995962, 0.995967, 0.995971, 0.995975, 0.995979, 
+            0.995984, 0.995988, 0.995992, 0.995996, 0.996, 0.996004, 0.996009, 0.996013, 
+            0.996017, 0.996021, 0.996025, 0.996029, 0.996033, 0.996037, 0.996041, 
+            0.996046, 0.99605, 0.996054, 0.996058, 0.996062, 0.996066, 0.99607, 0.996074, 
+            0.996078, 0.996082, 0.996086, 0.99609, 0.996094, 0.996098, 0.996102, 
+            0.996106, 0.99611, 0.996114, 0.996117, 0.996121, 0.996125, 0.996129, 
+            0.996133, 0.996137, 0.996141, 0.996145, 0.996149, 0.996152, 0.996156, 
+            0.99616, 0.996164]
+
+# Run-time configuration parameters (can be set with command-line options)
+
+$rerun=1
+$inner=3
+$warmup=1
+$outer=4
+$includeSunSpider=true
+$includeV8=true
+$includeKraken=true
+$measureGC=false
+$benchmarkPattern=nil
+$verbosity=0
+$timeMode=:preciseTime
+$forceVMKind=nil
+$brief=false
+$silent=false
+$remoteHosts=[]
+$alsoLocal=false
+$sshOptions=[]
+$vms = []
+$needToCopyVMs = false
+$dontCopyVMs = false
+
+$prepare = true
+$run = true
+$analyze = []
+
+# Helpful functions and classes
+
+def smallUsage
+  puts "Use the --help option to get basic usage information."
+  exit 1
+end
+
+def usage
+  puts "bencher [options] <vm1> [<vm2> ...]"
+  puts
+  puts "Runs one or more JavaScript runtimes against SunSpider, V8, and/or Kraken"
+  puts "benchmarks, and reports detailed statistics.  What makes bencher special is"
+  puts "that each benchmark/VM configuration is run in a single VM invocation, and"
+  puts "the invocations are run in random order.  This minimizes systematics due to"
+  puts "one benchmark polluting the running time of another.  The fine-grained"
+  puts "interleaving of VM invocations further minimizes systematics due to changes in"
+  puts "the performance or behavior of your machine."
+  puts 
+  puts "Bencher is highly configurable.  You can compare as many VMs as you like.  You"
+  puts "can change the amount of warm-up iterations, number of iterations executed per"
+  puts "VM invocation, and the number of VM invocations per benchmark.  By default,"
+  puts "SunSpider, VM, and Kraken are all run; but you can run any combination of these"
+  puts "suites."
+  puts
+  puts "The <vm> should be either a path to a JavaScript runtime executable (such as"
+  puts "jsc), or a string of the form <name>:<path>, where the <path> is the path to"
+  puts "the executable and <name> is the name that you would like to give the"
+  puts "configuration for the purposeof reporting.  If no name is given, a generic name"
+  puts "of the form Conf#<n> will be ascribed to the configuration automatically."
+  puts
+  puts "Options:"
+  puts "--rerun <n>          Set the number of iterations of the benchmark that"
+  puts "                     contribute to the measured run time.  Default is #{$rerun}."
+  puts "--inner <n>          Set the number of inner (per-runtime-invocation)"
+  puts "                     iterations.  Default is #{$inner}."
+  puts "--outer <n>          Set the number of runtime invocations for each benchmark."
+  puts "                     Default is #{$outer}."
+  puts "--warmup <n>         Set the number of warm-up runs per invocation.  Default"
+  puts "                     is #{$warmup}."
+  puts "--timing-mode        Set the way that bencher measures time.  Possible values"
+  puts "                     are 'preciseTime' and 'date'.  Default is 'preciseTime'."
+  puts "--force-vm-kind      Turn off auto-detection of VM kind, and assume that it is"
+  puts "                     the one specified.  Valid arguments are 'jsc' or"
+  puts "                     'DumpRenderTree'."
+  puts "--force-vm-copy      Force VM builds to be copied to bencher's working directory."
+  puts "                     This may reduce pathologies resulting from path names."
+  puts "--dont-copy-vms      Don't copy VMs even when doing a remote benchmarking run;"
+  puts "                     instead assume that they are already there."
+  puts "--v8-only            Only run V8."
+  puts "--sunspider-only     Only run SunSpider."
+  puts "--kraken-only        Only run Kraken."
+  puts "--exclude-v8         Exclude V8 (only run SunSpider and Kraken)."
+  puts "--exclude-sunspider  Exclude SunSpider (only run V8 and Kraken)."
+  puts "--exclude-kraken     Exclude Kraken (only run SunSpider and V8)."
+  puts "--benchmarks         Only run benchmarks matching the given regular expression."
+  puts "--measure-gc         Turn off manual calls to gc(), so that GC time is measured."
+  puts "                     Works best with large values of --inner.  You can also say"
+  puts "                     --measure-gc <conf>, which turns this on for one"
+  puts "                     configuration only."
+  puts "--verbose or -v      Print more stuff."
+  puts "--brief              Print only the final result for each VM."
+  puts "--silent             Don't print progress. This might slightly reduce some"
+  puts "                     performance perturbation."
+  puts "--remote <sshhosts>  Performance performance measurements remotely, on the given"
+  puts "                     SSH host(s). Easiest way to use this is to specify the SSH"
+  puts "                     user@host string. However, you can also supply a comma-"
+  puts "                     separated list of SSH hosts. Alternatively, you can use this"
+  puts "                     option multiple times to specify multiple hosts. This"
+  puts "                     automatically copies the WebKit release builds of the VMs"
+  puts "                     you specified to all of the hosts."
+  puts "--ssh-options        Pass additional options to SSH."
+  puts "--local              Also do a local benchmark run even when doing --remote."
+  puts "--prepare-only       Only prepare the bencher runscript (a shell script that"
+  puts "                     invokes the VMs to run benchmarks) but don't run it."
+  puts "--analyze            Only read the output of the runscript but don't do anything"
+  puts "                     else. This requires passing the same arguments to bencher"
+  puts "                     that you passed when running --prepare-only."
+  puts "--help or -h         Display this message."
+  puts
+  puts "Example:"
+  puts "bencher TipOfTree:/Volumes/Data/pizlo/OpenSource/WebKitBuild/Release/jsc MyChanges:/Volumes/Data/pizlo/secondary/OpenSource/WebKitBuild/Release/jsc"
+  exit 1
+end
+
+def fail(reason)
+  if reason.respond_to? :backtrace
+    puts "FAILED: #{reason}"
+    puts "Stack trace:"
+    puts reason.backtrace.join("\n")
+  else
+    puts "FAILED: #{reason}"
+  end
+  smallUsage
+end
+
+def quickFail(r1,r2)
+  $stderr.puts "#{$0}: #{r1}"
+  puts
+  fail(r2)
+end
+
+def intArg(argName,arg,min,max)
+  result=arg.to_i
+  unless result.to_s == arg
+    quickFail("Expected an integer value for #{argName}, but got #{arg}.",
+              "Invalid argument for command-line option")
+  end
+  if min and result<min
+    quickFail("Argument for #{argName} cannot be smaller than #{min}.",
+              "Invalid argument for command-line option")
+  end
+  if max and result>max
+    quickFail("Argument for #{argName} cannot be greater than #{max}.",
+              "Invalid argument for command-line option")
+  end
+  result
+end
+
+def computeMean(array)
+  sum=0.0
+  array.each {
+    | value |
+    sum += value
+  }
+  sum/array.length
+end
+
+def computeGeometricMean(array)
+  mult=1.0
+  array.each {
+    | value |
+    mult*=value
+  }
+  mult**(1.0/array.length)
+end
+
+def computeHarmonicMean(array)
+  1.0 / computeMean(array.collect{ | value | 1.0 / value })
+end
+
+def computeStdDev(array)
+  case array.length
+  when 0
+    0.0/0.0
+  when 1
+    0.0
+  else
+    begin
+      mean=computeMean(array)
+      sum=0.0
+      array.each {
+        | value |
+        sum += (value-mean)**2
+      }
+      Math.sqrt(sum/(array.length-1))
+    rescue
+      0.0/0.0
+    end
+  end
+end
+
+class Array
+  def shuffle!
+    size.downto(1) { |n| push delete_at(rand(n)) }
+    self
+  end
+end
+
+def inverseBetaRegularized(n)
+  IBR_LOOKUP[n-1]
+end
+
+def numToStr(num)
+  "%.4f"%(num.to_f)
+end
+  
+class NoChange
+  attr_reader :amountFaster
+  
+  def initialize(amountFaster)
+    @amountFaster = amountFaster
+  end
+  
+  def shortForm
+    " "
+  end
+  
+  def longForm
+    "  might be #{numToStr(@amountFaster)}x faster"
+  end
+  
+  def to_s
+    if @amountFaster < 1.01
+      ""
+    else
+      longForm
+    end
+  end
+end
+
+class Faster
+  attr_reader :amountFaster
+  
+  def initialize(amountFaster)
+    @amountFaster = amountFaster
+  end
+  
+  def shortForm
+    "^"
+  end
+  
+  def longForm
+    "^ definitely #{numToStr(@amountFaster)}x faster"
+  end
+  
+  def to_s
+    longForm
+  end
+end
+
+class Slower
+  attr_reader :amountSlower
+  
+  def initialize(amountSlower)
+    @amountSlower = amountSlower
+  end
+  
+  def shortForm
+    "!"
+  end
+  
+  def longForm
+    "! definitely #{numToStr(@amountSlower)}x slower"
+  end
+  
+  def to_s
+    longForm
+  end
+end
+
+class MayBeSlower
+  attr_reader :amountSlower
+  
+  def initialize(amountSlower)
+    @amountSlower = amountSlower
+  end
+  
+  def shortForm
+    "?"
+  end
+  
+  def longForm
+    "? might be #{numToStr(@amountSlower)}x slower"
+  end
+  
+  def to_s
+    if @amountSlower < 1.01
+      "?"
+    else
+      longForm
+    end
+  end
+end
+
+class Stats
+  def initialize
+    @array = []
+  end
+  
+  def add(value)
+    if value.is_a? Stats
+      add(value.array)
+    elsif value.respond_to? :each
+      value.each {
+        | v |
+        add(v)
+      }
+    else
+      @array << value.to_f
+    end
+  end
+    
+  def array
+    @array
+  end
+  
+  def sum
+    result=0
+    @array.each {
+      | value |
+      result += value
+    }
+    result
+  end
+  
+  def min
+    @array.min
+  end
+  
+  def max
+    @array.max
+  end
+  
+  def size
+    @array.length
+  end
+  
+  def mean
+    computeMean(array)
+  end
+  
+  def arithmeticMean
+    mean
+  end
+  
+  def stdDev
+    computeStdDev(array)
+  end
+
+  def stdErr
+    stdDev/Math.sqrt(size)
+  end
+  
+  # Computes a 95% Student's t distribution confidence interval
+  def confInt
+    if size < 2
+      0.0/0.0
+    else
+      raise if size > 1000
+      Math.sqrt(size-1.0)*stdErr*Math.sqrt(-1.0+1.0/inverseBetaRegularized(size-1))
+    end
+  end
+  
+  def lower
+    mean-confInt
+  end
+  
+  def upper
+    mean+confInt
+  end
+  
+  def geometricMean
+    computeGeometricMean(array)
+  end
+  
+  def harmonicMean
+    computeHarmonicMean(array)
+  end
+  
+  def compareTo(other)
+    if upper < other.lower
+      Faster.new(other.mean/mean)
+    elsif lower > other.upper
+      Slower.new(mean/other.mean)
+    elsif mean > other.mean
+      MayBeSlower.new(mean/other.mean)
+    else
+      NoChange.new(other.mean/mean)
+    end
+  end
+  
+  def to_s
+    "size = #{size}, mean = #{mean}, stdDev = #{stdDev}, stdErr = #{stdErr}, confInt = #{confInt}"
+  end
+end
+
+def doublePuts(out1,out2,msg)
+  out1.puts "#{out2.path}: #{msg}" if $verbosity>=3
+  out2.puts msg
+end
+
+class Benchfile < File
+  @@counter = 0
+  
+  attr_reader :filename, :basename
+  
+  def initialize(name)
+    @basename, @filename = Benchfile.uniqueFilename(name)
+    super(@filename, "w")
+  end
+  
+  def self.uniqueFilename(name)
+    if name.is_a? Array
+      basename = name[0] + @@counter.to_s + name[1]
+    else
+      basename = name + @@counter.to_s
+    end
+    filename = BENCH_DATA_PATH + "/" + basename
+    @@counter += 1
+    raise "Benchfile #{filename} already exists" if FileTest.exist?(filename)
+    [basename, filename]
+  end
+  
+  def self.create(name)
+    file = Benchfile.new(name)
+    yield file
+    file.close
+    file.basename
+  end
+end
+
+$dataFiles={}
+def ensureFile(key, filename)
+  unless $dataFiles[key]
+    $dataFiles[key] = Benchfile.create(key) {
+      | outp |
+      doublePuts($stderr,outp,IO::read(filename))
+    }
+  end
+  $dataFiles[key]
+end
+  
+def emitBenchRunCodeFile(name, plan, benchDataPath, benchPath)
+  case plan.vm.vmType
+  when :jsc
+    Benchfile.create("bencher") {
+      | file |
+      case $timeMode
+      when :preciseTime
+        doublePuts($stderr,file,"function __bencher_curTimeMS() {")
+        doublePuts($stderr,file,"   return preciseTime()*1000")
+        doublePuts($stderr,file,"}")
+      when :date
+        doublePuts($stderr,file,"function __bencher_curTimeMS() {")
+        doublePuts($stderr,file,"   return Date.now()")
+        doublePuts($stderr,file,"}")
+      else
+        raise
+      end
+      
+      if benchDataPath
+        doublePuts($stderr,file,"load(#{benchDataPath.inspect});")
+        doublePuts($stderr,file,"gc();")
+        doublePuts($stderr,file,"for (var __bencher_index = 0; __bencher_index < #{$warmup+$inner}; ++__bencher_index) {")
+        doublePuts($stderr,file,"   before = __bencher_curTimeMS();")
+        $rerun.times {
+          doublePuts($stderr,file,"   load(#{benchPath.inspect});")
+        }
+        doublePuts($stderr,file,"   after = __bencher_curTimeMS();")
+        doublePuts($stderr,file,"   if (__bencher_index >= #{$warmup}) print(\"#{name}: #{plan.vm}: #{plan.iteration}: \" + (__bencher_index - #{$warmup}) + \": Time: \"+(after-before));");
+        doublePuts($stderr,file,"   gc();") unless plan.vm.shouldMeasureGC
+        doublePuts($stderr,file,"}")
+      else
+        doublePuts($stderr,file,"function __bencher_run(__bencher_what) {")
+        doublePuts($stderr,file,"   var __bencher_before = __bencher_curTimeMS();")
+        $rerun.times {
+          doublePuts($stderr,file,"   run(__bencher_what);")
+        }
+        doublePuts($stderr,file,"   var __bencher_after = __bencher_curTimeMS();")
+        doublePuts($stderr,file,"   return __bencher_after - __bencher_before;")
+        doublePuts($stderr,file,"}")
+        $warmup.times {
+          doublePuts($stderr,file,"__bencher_run(#{benchPath.inspect})")
+          doublePuts($stderr,file,"gc();") unless plan.vm.shouldMeasureGC
+        }
+        $inner.times {
+          | innerIndex |
+          doublePuts($stderr,file,"print(\"#{name}: #{plan.vm}: #{plan.iteration}: #{innerIndex}: Time: \"+__bencher_run(#{benchPath.inspect}));")
+          doublePuts($stderr,file,"gc();") unless plan.vm.shouldMeasureGC
+        }
+      end
+    }
+  when :dumpRenderTree
+    mainCode = Benchfile.create("bencher") {
+      | file |
+      doublePuts($stderr,file,"__bencher_count = 0;")
+      doublePuts($stderr,file,"function __bencher_doNext(result) {")
+      doublePuts($stderr,file,"    if (__bencher_count >= #{$warmup})")
+      doublePuts($stderr,file,"        debug(\"#{name}: #{plan.vm}: #{plan.iteration}: \" + (__bencher_count - #{$warmup}) + \": Time: \" + result);")
+      doublePuts($stderr,file,"    __bencher_count++;")
+      doublePuts($stderr,file,"    if (__bencher_count < #{$inner+$warmup})")
+      doublePuts($stderr,file,"        __bencher_runImpl(__bencher_doNext);")
+      doublePuts($stderr,file,"    else")
+      doublePuts($stderr,file,"        quit();")
+      doublePuts($stderr,file,"}")
+      doublePuts($stderr,file,"__bencher_runImpl(__bencher_doNext);")
+    }
+    
+    cssCode = Benchfile.create("bencher-css") {
+      | file |
+      doublePuts($stderr,file,".pass {\n    font-weight: bold;\n    color: green;\n}\n.fail {\n    font-weight: bold;\n    color: red;\n}\n\#console {\n    white-space: pre-wrap;\n    font-family: monospace;\n}")
+    }
+    
+    preCode = Benchfile.create("bencher-pre") {
+      | file |
+      doublePuts($stderr,file,"if (window.testRunner) {")
+      doublePuts($stderr,file,"    testRunner.dumpAsText(window.enablePixelTesting);")
+      doublePuts($stderr,file,"    testRunner.waitUntilDone();")
+      doublePuts($stderr,file,"}")
+      doublePuts($stderr,file,"")
+      doublePuts($stderr,file,"function debug(msg)")
+      doublePuts($stderr,file,"{")
+      doublePuts($stderr,file,"    var span = document.createElement(\"span\");")
+      doublePuts($stderr,file,"    document.getElementById(\"console\").appendChild(span); // insert it first so XHTML knows the namespace")
+      doublePuts($stderr,file,"    span.innerHTML = msg + '<br />';")
+      doublePuts($stderr,file,"}")
+      doublePuts($stderr,file,"")
+      doublePuts($stderr,file,"function quit() {")
+      doublePuts($stderr,file,"    testRunner.notifyDone();")
+      doublePuts($stderr,file,"}")
+      doublePuts($stderr,file,"")
+      doublePuts($stderr,file,"__bencher_continuation=null;")
+      doublePuts($stderr,file,"")
+      doublePuts($stderr,file,"function reportResult(result) {")
+      doublePuts($stderr,file,"    __bencher_continuation(result);")
+      doublePuts($stderr,file,"}")
+      doublePuts($stderr,file,"")
+      doublePuts($stderr,file,"function __bencher_runImpl(continuation) {")
+      doublePuts($stderr,file,"    function doit() {")
+      doublePuts($stderr,file,"        document.getElementById(\"frameparent\").innerHTML = \"\";")
+      doublePuts($stderr,file,"        document.getElementById(\"frameparent\").innerHTML = \"<iframe id='testframe'>\";")
+      doublePuts($stderr,file,"        var testFrame = document.getElementById(\"testframe\");")
+      doublePuts($stderr,file,"        testFrame.contentDocument.open();")
+      doublePuts($stderr,file,"        testFrame.contentDocument.write(\"<!DOCTYPE html>\\n<head></head><body><div id=\\\"console\\\"></div>\");")
+      if benchDataPath
+        doublePuts($stderr,file,"        testFrame.contentDocument.write(\"<script src=\\\"#{benchDataPath}\\\"></script>\");")
+      end
+      doublePuts($stderr,file,"        testFrame.contentDocument.write(\"<script type=\\\"text/javascript\\\">__bencher_before = Date.now();</script><script src=\\\"#{benchPath}\\\"></script><script type=\\\"text/javascript\\\">window.parent.reportResult(Date.now() - __bencher_before);</script></body></html>\");")
+      doublePuts($stderr,file,"        testFrame.contentDocument.close();")
+      doublePuts($stderr,file,"    }")
+      doublePuts($stderr,file,"    __bencher_continuation = continuation;")
+      doublePuts($stderr,file,"    window.setTimeout(doit, 10);")
+      doublePuts($stderr,file,"}")
+    }
+
+    Benchfile.create(["bencher-htmldoc",".html"]) {
+      | file |
+      doublePuts($stderr,file,"<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML//EN\">\n<html><head><link rel=\"stylesheet\" href=\"#{cssCode}\"><script src=\"#{preCode}\"></script></head><body><div id=\"console\"></div><div id=\"frameparent\"></div><script src=\"#{mainCode}\"></script></body></html>")
+    }
+  else
+    raise
+  end
+end
+
+def emitBenchRunCode(name, plan, benchDataPath, benchPath)
+  plan.vm.emitRunCode(emitBenchRunCodeFile(name, plan, benchDataPath, benchPath))
+end
+
+def planForDescription(plans, benchFullname, vmName, iteration)
+  raise unless benchFullname =~ /\//
+  suiteName = $~.pre_match
+  benchName = $~.post_match
+  result = plans.select{|v| v.suite.name == suiteName and v.benchmark.name == benchName and v.vm.name == vmName and v.iteration == iteration}
+  raise unless result.size == 1
+  result[0]
+end
+
+class ParsedResult
+  attr_reader :plan, :innerIndex, :time
+  
+  def initialize(plan, innerIndex, time)
+    @plan = plan
+    @innerIndex = innerIndex
+    @time = time
+    
+    raise unless @plan.is_a? BenchPlan
+    raise unless @innerIndex.is_a? Integer
+    raise unless @time.is_a? Numeric
+  end
+  
+  def benchmark
+    plan.benchmark
+  end
+  
+  def suite
+    plan.suite
+  end
+  
+  def vm
+    plan.vm
+  end
+  
+  def outerIndex
+    plan.iteration
+  end
+  
+  def self.parse(plans, string)
+    if string =~ /([a-zA-Z0-9\/-]+): ([a-zA-Z0-9_# ]+): ([0-9]+): ([0-9]+): Time: /
+      benchFullname = $1
+      vmName = $2
+      outerIndex = $3.to_i
+      innerIndex = $4.to_i
+      time = $~.post_match.to_f
+      ParsedResult.new(planForDescription(plans, benchFullname, vmName, outerIndex), innerIndex, time)
+    else
+      nil
+    end
+  end
+end
+
+class VM
+  def initialize(origPath, name, nameKind, svnRevision)
+    @origPath = origPath.to_s
+    @path = origPath.to_s
+    @name = name
+    @nameKind = nameKind
+    
+    if $forceVMKind
+      @vmType = $forceVMKind
+    else
+      if @origPath =~ /DumpRenderTree$/
+        @vmType = :dumpRenderTree
+      else
+        @vmType = :jsc
+      end
+    end
+    
+    @svnRevision = svnRevision
+    
+    # Try to detect information about the VM.
+    if path =~ /\/WebKitBuild\/Release\/([a-zA-Z]+)$/
+      @checkoutPath = $~.pre_match
+      # FIXME: Use some variant of this: 
+      # <bdash>   def retrieve_revision
+      # <bdash>     `perl -I#{@path}/Tools/Scripts -MVCSUtils -e 'print svnRevisionForDirectory("#{@path}");'`.to_i
+      # <bdash>   end
+      unless @svnRevision
+        begin
+          Dir.chdir(@checkoutPath) {
+            $stderr.puts ">> cd #{@checkoutPath} && svn info" if $verbosity>=2
+            IO.popen("svn info", "r") {
+              | inp |
+              inp.each_line {
+                | line |
+                if line =~ /Revision: ([0-9]+)/
+                  @svnRevision = $1
+                end
+              }
+            }
+          }
+          unless @svnRevision
+            $stderr.puts "Warning: running svn info for #{name} silently failed."
+          end
+        rescue => e
+          # Failed to detect svn revision.
+          $stderr.puts "Warning: could not get svn revision information for #{name}: #{e}"
+        end
+      end
+    else
+      $stderr.puts "Warning: could not identify checkout location for #{name}"
+    end
+    
+    if @path =~ /\/Release\/([a-zA-Z]+)$/
+      @libPath, @relativeBinPath = $~.pre_match+"/Release", "./#{$1}"
+    elsif @path =~ /\/Contents\/Resources\/([a-zA-Z]+)$/
+      @libPath = $~.pre_match
+    elsif @path =~ /\/JavaScriptCore.framework\/Resources\/([a-zA-Z]+)$/
+      @libPath, @relativeBinPath = $~.pre_match, $&[1..-1]
+    end
+  end
+  
+  def canCopyIntoBenchPath
+    if @libPath and @relativeBinPath
+      true
+    else
+      false
+    end
+  end
+  
+  def copyIntoBenchPath
+    raise unless canCopyIntoBenchPath
+    basename, filename = Benchfile.uniqueFilename("vm")
+    raise unless Dir.mkdir(filename)
+    cmd = "cp -a #{@libPath.inspect}/* #{filename.inspect}"
+    $stderr.puts ">> #{cmd}" if $verbosity>=2
+    raise unless system(cmd)
+    @path = "#{basename}/#{@relativeBinPath}"
+    @libPath = basename
+  end
+  
+  def to_s
+    @name
+  end
+  
+  def name
+    @name
+  end
+  
+  def shouldMeasureGC
+    $measureGC == true or ($measureGC == name)
+  end
+  
+  def origPath
+    @origPath
+  end
+  
+  def path
+    @path
+  end
+  
+  def nameKind
+    @nameKind
+  end
+  
+  def vmType
+    @vmType
+  end
+  
+  def checkoutPath
+    @checkoutPath
+  end
+  
+  def svnRevision
+    @svnRevision
+  end
+  
+  def printFunction
+    case @vmType
+    when :jsc
+      "print"
+    when :dumpRenderTree
+      "debug"
+    else
+      raise @vmType
+    end
+  end
+  
+  def emitRunCode(fileToRun)
+    myLibPath = @libPath
+    myLibPath = "" unless myLibPath
+    $script.puts "export DYLD_LIBRARY_PATH=#{myLibPath.to_s.inspect}"
+    $script.puts "export DYLD_FRAMEWORK_PATH=#{myLibPath.to_s.inspect}"
+    $script.puts "#{path} #{fileToRun}"
+  end
+end
+
+class StatsAccumulator
+  def initialize
+    @stats = []
+    ($outer*$inner).times {
+      @stats << Stats.new
+    }
+  end
+  
+  def statsForIteration(outerIteration, innerIteration)
+    @stats[outerIteration*$inner + innerIteration]
+  end
+  
+  def stats
+    result = Stats.new
+    @stats.each {
+      | stat |
+      result.add(yield stat)
+    }
+    result
+  end
+  
+  def geometricMeanStats
+    stats {
+      | stat |
+      stat.geometricMean
+    }
+  end
+  
+  def arithmeticMeanStats
+    stats {
+      | stat |
+      stat.arithmeticMean
+    }
+  end
+end
+
+module Benchmark
+  attr_accessor :benchmarkSuite
+  attr_reader :name
+  
+  def fullname
+    benchmarkSuite.name + "/" + name
+  end
+  
+  def to_s
+    fullname
+  end
+end
+
+class SunSpiderBenchmark
+  include Benchmark
+  
+  def initialize(name)
+    @name = name
+  end
+  
+  def emitRunCode(plan)
+    emitBenchRunCode(fullname, plan, nil, ensureFile("SunSpider-#{@name}", "#{SUNSPIDER_PATH}/#{@name}.js"))
+  end
+end
+
+class V8Benchmark
+  include Benchmark
+  
+  def initialize(name)
+    @name = name
+  end
+  
+  def emitRunCode(plan)
+    emitBenchRunCode(fullname, plan, nil, ensureFile("V8-#{@name}", "#{V8_PATH}/v8-#{@name}.js"))
+  end
+end
+
+class KrakenBenchmark
+  include Benchmark
+  
+  def initialize(name)
+    @name = name
+  end
+  
+  def emitRunCode(plan)
+    emitBenchRunCode(fullname, plan, ensureFile("KrakenData-#{@name}", "#{KRAKEN_PATH}/#{@name}-data.js"), ensureFile("Kraken-#{@name}", "#{KRAKEN_PATH}/#{@name}.js"))
+  end
+end
+
+class BenchmarkSuite
+  def initialize(name, path, preferredMean)
+    @name = name
+    @path = path
+    @preferredMean = preferredMean
+    @benchmarks = []
+  end
+  
+  def name
+    @name
+  end
+  
+  def to_s
+    @name
+  end
+  
+  def path
+    @path
+  end
+  
+  def add(benchmark)
+    if not $benchmarkPattern or "#{@name}/#{benchmark.name}" =~ $benchmarkPattern
+      benchmark.benchmarkSuite = self
+      @benchmarks << benchmark
+    end
+  end
+  
+  def benchmarks
+    @benchmarks
+  end
+  
+  def benchmarkForName(name)
+    result = @benchmarks.select{|v| v.name == name}
+    raise unless result.length == 1
+    result[0]
+  end
+  
+  def empty?
+    @benchmarks.empty?
+  end
+  
+  def retain_if
+    @benchmarks.delete_if {
+      | benchmark |
+      not yield benchmark
+    }
+  end
+  
+  def preferredMean
+    @preferredMean
+  end
+  
+  def computeMean(stat)
+    stat.send @preferredMean
+  end
+end
+
+class BenchRunPlan
+  def initialize(benchmark, vm, iteration)
+    @benchmark = benchmark
+    @vm = vm
+    @iteration = iteration
+  end
+  
+  def benchmark
+    @benchmark
+  end
+  
+  def suite
+    @benchmark.benchmarkSuite
+  end
+  
+  def vm
+    @vm
+  end
+  
+  def iteration
+    @iteration
+  end
+  
+  def emitRunCode
+    @benchmark.emitRunCode(self)
+  end
+end
+
+class BenchmarkOnVM
+  def initialize(benchmark, suiteOnVM)
+    @benchmark = benchmark
+    @suiteOnVM = suiteOnVM
+    @stats = Stats.new
+  end
+  
+  def to_s
+    "#{@benchmark} on #{@suiteOnVM.vm}"
+  end
+  
+  def benchmark
+    @benchmark
+  end
+  
+  def vm
+    @suiteOnVM.vm
+  end
+  
+  def vmStats
+    @suiteOnVM.vmStats
+  end
+  
+  def suite
+    @benchmark.benchmarkSuite
+  end
+  
+  def suiteOnVM
+    @suiteOnVM
+  end
+  
+  def stats
+    @stats
+  end
+  
+  def parseResult(result)
+    raise "VM mismatch; I've got #{vm} and they've got #{result.vm}" unless result.vm == vm
+    raise unless result.benchmark == @benchmark
+    @stats.add(result.time)
+  end
+end
+
+class SuiteOnVM < StatsAccumulator
+  def initialize(vm, vmStats, suite)
+    super()
+    @vm = vm
+    @vmStats = vmStats
+    @suite = suite
+    
+    raise unless @vm.is_a? VM
+    raise unless @vmStats.is_a? StatsAccumulator
+    raise unless @suite.is_a? BenchmarkSuite
+  end
+  
+  def to_s
+    "#{@suite} on #{@vm}"
+  end
+  
+  def suite
+    @suite
+  end
+  
+  def vm
+    @vm
+  end
+  
+  def vmStats
+    raise unless @vmStats
+    @vmStats
+  end
+end
+
+class BenchPlan
+  def initialize(benchmarkOnVM, iteration)
+    @benchmarkOnVM = benchmarkOnVM
+    @iteration = iteration
+  end
+  
+  def to_s
+    "#{@benchmarkOnVM} \##{@iteration+1}"
+  end
+  
+  def benchmarkOnVM
+    @benchmarkOnVM
+  end
+  
+  def benchmark
+    @benchmarkOnVM.benchmark
+  end
+  
+  def suite
+    @benchmarkOnVM.suite
+  end
+  
+  def vm
+    @benchmarkOnVM.vm
+  end
+  
+  def iteration
+    @iteration
+  end
+  
+  def parseResult(result)
+    raise unless result.plan == self
+    @benchmarkOnVM.parseResult(result)
+    @benchmarkOnVM.vmStats.statsForIteration(@iteration, result.innerIndex).add(result.time)
+    @benchmarkOnVM.suiteOnVM.statsForIteration(@iteration, result.innerIndex).add(result.time)
+  end
+end
+
+def lpad(str,chars)
+  if str.length>chars
+    str
+  else
+    "%#{chars}s"%(str)
+  end
+end
+
+def rpad(str,chars)
+  while str.length<chars
+    str+=" "
+  end
+  str
+end
+
+def center(str,chars)
+  while str.length<chars
+    str+=" "
+    if str.length<chars
+      str=" "+str
+    end
+  end
+  str
+end
+
+def statsToStr(stats)
+  if $inner*$outer == 1
+    string = numToStr(stats.mean)
+    raise unless string =~ /\./
+    left = $~.pre_match
+    right = $~.post_match
+    lpad(left,12)+"."+rpad(right,9)
+  else
+    lpad(numToStr(stats.mean),11)+"+-"+rpad(numToStr(stats.confInt),9)
+  end
+end
+
+def plural(num)
+  if num == 1
+    ""
+  else
+    "s"
+  end
+end
+
+def wrap(str, columns)
+  array = str.split
+  result = ""
+  curLine = array.shift
+  array.each {
+    | curStr |
+    if (curLine + " " + curStr).size > columns
+      result += curLine + "\n"
+      curLine = curStr
+    else
+      curLine += " " + curStr
+    end
+  }
+  result + curLine + "\n"
+end
+  
+def runAndGetResults
+  results = nil
+  Dir.chdir(BENCH_DATA_PATH) {
+    IO.popen("sh ./runscript", "r") {
+      | inp |
+      results = inp.read
+    }
+    raise "Script did not complete correctly: #{$?}" unless $?.success?
+  }
+  raise unless results
+  results
+end
+
+def parseAndDisplayResults(results)
+  vmStatses = []
+  $vms.each {
+    vmStatses << StatsAccumulator.new
+  }
+  
+  suitesOnVMs = []
+  suitesOnVMsForSuite = {}
+  $suites.each {
+    | suite |
+    suitesOnVMsForSuite[suite] = []
+  }
+  suitesOnVMsForVM = {}
+  $vms.each {
+    | vm |
+    suitesOnVMsForVM[vm] = []
+  }
+  
+  benchmarksOnVMs = []
+  benchmarksOnVMsForBenchmark = {}
+  $benchmarks.each {
+    | benchmark |
+    benchmarksOnVMsForBenchmark[benchmark] = []
+  }
+  
+  $vms.each_with_index {
+    | vm, vmIndex |
+    vmStats = vmStatses[vmIndex]
+    $suites.each {
+      | suite |
+      suiteOnVM = SuiteOnVM.new(vm, vmStats, suite)
+      suitesOnVMs << suiteOnVM
+      suitesOnVMsForSuite[suite] << suiteOnVM
+      suitesOnVMsForVM[vm] << suiteOnVM
+      suite.benchmarks.each {
+        | benchmark |
+        benchmarkOnVM = BenchmarkOnVM.new(benchmark, suiteOnVM)
+        benchmarksOnVMs << benchmarkOnVM
+        benchmarksOnVMsForBenchmark[benchmark] << benchmarkOnVM
+      }
+    }
+  }
+  
+  plans = []
+  benchmarksOnVMs.each {
+    | benchmarkOnVM |
+    $outer.times {
+      | iteration |
+      plans << BenchPlan.new(benchmarkOnVM, iteration)
+    }
+  }
+
+  hostname = nil
+  hwmodel = nil
+  results.each_line {
+    | line |
+    line.chomp!
+    if line =~ /HOSTNAME:([^.]+)/
+      hostname = $1
+    elsif line =~ /HARDWARE:hw\.model: /
+      hwmodel = $~.post_match.chomp
+    else
+      result = ParsedResult.parse(plans, line.chomp)
+      if result
+        result.plan.parseResult(result)
+      end
+    end
+  }
+  
+  # Compute the geomean of the preferred means of results on a SuiteOnVM
+  overallResults = []
+  $vms.each {
+    | vm |
+    result = Stats.new
+    $outer.times {
+      | outerIndex |
+      $inner.times {
+        | innerIndex |
+        curResult = Stats.new
+        suitesOnVMsForVM[vm].each {
+          | suiteOnVM |
+          # For a given iteration, suite, and VM, compute the suite's preferred mean
+          # over the data collected for all benchmarks in that suite. We'll have one
+          # sample per benchmark. For example on V8 this will be the geomean of 1
+          # sample for crypto, 1 sample for deltablue, and so on, and 1 sample for
+          # splay.
+          curResult.add(suiteOnVM.suite.computeMean(suiteOnVM.statsForIteration(outerIndex, innerIndex)))
+        }
+        
+        # curResult now holds 1 sample for each of the means computed in the above
+        # loop. Compute the geomean over this, and store it.
+        result.add(curResult.geometricMean)
+      }
+    }
+
+    # $overallResults will have a Stats for each VM. That Stats object will hold
+    # $inner*$outer geomeans, allowing us to compute the arithmetic mean and
+    # confidence interval of the geomeans of preferred means. Convoluted, but
+    # useful and probably sound.
+    overallResults << result
+  }
+  
+  if $verbosity >= 2
+    benchmarksOnVMs.each {
+      | benchmarkOnVM |
+      $stderr.puts "#{benchmarkOnVM}: #{benchmarkOnVM.stats}"
+    }
+    
+    $vms.each_with_index {
+      | vm, vmIndex |
+      vmStats = vmStatses[vmIndex]
+      $stderr.puts "#{vm} (arithmeticMean): #{vmStats.arithmeticMeanStats}"
+      $stderr.puts "#{vm} (geometricMean): #{vmStats.geometricMeanStats}"
+    }
+  end
+
+  reportName =
+    (if ($vms.collect {
+           | vm |
+           vm.nameKind
+         }.index :auto)
+       ""
+     else
+       $vms.collect {
+         | vm |
+         vm.to_s
+       }.join("_") + "_"
+     end) +
+    ($suites.collect {
+       | suite |
+       suite.to_s
+     }.join("")) + "_" +
+    (if hostname
+       hostname + "_"
+     else
+       ""
+     end)+
+    (begin
+       time = Time.now
+       "%04d%02d%02d_%02d%02d" %
+         [ time.year, time.month, time.day,
+           time.hour, time.min ]
+     end) +
+    "_benchReport.txt"
+
+  unless $brief
+    puts "Generating benchmark report at #{reportName}"
+  end
+  
+  outp = $stdout
+  begin
+    outp = File.open(reportName,"w")
+  rescue => e
+    $stderr.puts "Error: could not save report to #{reportName}: #{e}"
+    $stderr.puts
+  end
+  
+  def createVMsString
+    result = ""
+    result += "   " if $suites.size > 1
+    result += rpad("", $benchpad)
+    result += " "
+    $vms.size.times {
+      | index |
+      if index != 0
+        result += " "+NoChange.new(0).shortForm
+      end
+      result += lpad(center($vms[index].name, 9+9+2), 11+9+2)
+    }
+    result += "    "
+    if $vms.size >= 3
+      result += center("#{$vms[-1].name} v. #{$vms[0].name}",26)
+    elsif $vms.size >= 2
+      result += " "*26
+    end
+    result
+  end
+  
+  columns = [createVMsString.size, 78].max
+  
+  outp.print "Benchmark report for "
+  if $suites.size == 1
+    outp.print $suites[0].to_s
+  elsif $suites.size == 2
+    outp.print "#{$suites[0]} and #{$suites[1]}"
+  else
+    outp.print "#{$suites[0..-2].join(', ')}, and #{$suites[-1]}"
+  end
+  if hostname
+    outp.print " on #{hostname}"
+  end
+  if hwmodel
+    outp.print " (#{hwmodel})"
+  end
+  outp.puts "."
+  outp.puts
+  
+  # This looks stupid; revisit later.
+  if false
+    $suites.each {
+      | suite |
+      outp.puts "#{suite} at #{suite.path}"
+    }
+    
+    outp.puts
+  end
+  
+  outp.puts "VMs tested:"
+  $vms.each {
+    | vm |
+    outp.print "\"#{vm.name}\" at #{vm.origPath}"
+    if vm.svnRevision
+      outp.print " (r#{vm.svnRevision})"
+    end
+    outp.puts
+  }
+  
+  outp.puts
+  
+  outp.puts wrap("Collected #{$outer*$inner} sample#{plural($outer*$inner)} per benchmark/VM, "+
+                 "with #{$outer} VM invocation#{plural($outer)} per benchmark."+
+                 (if $rerun > 1 then (" Ran #{$rerun} benchmark iterations, and measured the "+
+                                      "total time of those iterations, for each sample.")
+                  else "" end)+
+                 (if $measureGC == true then (" No manual garbage collection invocations were "+
+                                              "emitted.")
+                  elsif $measureGC then (" Emitted a call to gc() between sample measurements for "+
+                                         "all VMs except #{$measureGC}.")
+                  else (" Emitted a call to gc() between sample measurements.") end)+
+                 (if $warmup == 0 then (" Did not include any warm-up iterations; measurements "+
+                                        "began with the very first iteration.")
+                  else (" Used #{$warmup*$rerun} benchmark iteration#{plural($warmup*$rerun)} per VM "+
+                        "invocation for warm-up.") end)+
+                 (case $timeMode
+                  when :preciseTime then (" Used the jsc-specific preciseTime() function to get "+
+                                          "microsecond-level timing.")
+                  when :date then (" Used the portable Date.now() method to get millisecond-"+
+                                   "level timing.")
+                  else raise end)+
+                 " Reporting benchmark execution times with 95% confidence "+
+                 "intervals in milliseconds.",
+                 columns)
+  
+  outp.puts
+  
+  def printVMs(outp)
+    outp.puts createVMsString
+  end
+  
+  def summaryStats(outp, accumulators, name, &proc)
+    outp.print "   " if $suites.size > 1
+    outp.print rpad(name, $benchpad)
+    outp.print " "
+    accumulators.size.times {
+      | index |
+      if index != 0
+        outp.print " "+accumulators[index].stats(&proc).compareTo(accumulators[index-1].stats(&proc)).shortForm
+      end
+      outp.print statsToStr(accumulators[index].stats(&proc))
+    }
+    if accumulators.size>=2
+      outp.print("    "+accumulators[-1].stats(&proc).compareTo(accumulators[0].stats(&proc)).longForm)
+    end
+    outp.puts
+  end
+  
+  def meanName(currentMean, preferredMean)
+    result = "<#{currentMean}>"
+    if "#{currentMean}Mean" == preferredMean.to_s
+      result += " *"
+    end
+    result
+  end
+  
+  def allSummaryStats(outp, accumulators, preferredMean)
+    summaryStats(outp, accumulators, meanName("arithmetic", preferredMean)) {
+      | stat |
+      stat.arithmeticMean
+    }
+    
+    summaryStats(outp, accumulators, meanName("geometric", preferredMean)) {
+      | stat |
+      stat.geometricMean
+    }
+    
+    summaryStats(outp, accumulators, meanName("harmonic", preferredMean)) {
+      | stat |
+      stat.harmonicMean
+    }
+  end
+  
+  $suites.each {
+    | suite |
+    printVMs(outp)
+    if $suites.size > 1
+      outp.puts "#{suite.name}:"
+    else
+      outp.puts
+    end
+    suite.benchmarks.each {
+      | benchmark |
+      outp.print "   " if $suites.size > 1
+      outp.print rpad(benchmark.name, $benchpad)
+      outp.print " "
+      myConfigs = benchmarksOnVMsForBenchmark[benchmark]
+      myConfigs.size.times {
+        | index |
+        if index != 0
+          outp.print " "+myConfigs[index].stats.compareTo(myConfigs[index-1].stats).shortForm
+        end
+        outp.print statsToStr(myConfigs[index].stats)
+      }
+      if $vms.size>=2
+        outp.print("    "+myConfigs[-1].stats.compareTo(myConfigs[0].stats).to_s)
+      end
+      outp.puts
+    }
+    outp.puts
+    allSummaryStats(outp, suitesOnVMsForSuite[suite], suite.preferredMean)
+    outp.puts if $suites.size > 1
+  }
+  
+  if $suites.size > 1
+    printVMs(outp)
+    outp.puts "All benchmarks:"
+    allSummaryStats(outp, vmStatses, nil)
+    
+    outp.puts
+    printVMs(outp)
+    outp.puts "Geomean of preferred means:"
+    outp.print "   "
+    outp.print rpad("<scaled-result>", $benchpad)
+    outp.print " "
+    $vms.size.times {
+      | index |
+      if index != 0
+        outp.print " "+overallResults[index].compareTo(overallResults[index-1]).shortForm
+      end
+      outp.print statsToStr(overallResults[index])
+    }
+    if overallResults.size>=2
+      outp.print("    "+overallResults[-1].compareTo(overallResults[0]).longForm)
+    end
+    outp.puts
+  end
+  outp.puts
+  
+  if outp != $stdout
+    outp.close
+  end
+  
+  if outp != $stdout and not $brief
+    puts
+    File.open(reportName) {
+      | inp |
+      puts inp.read
+    }
+  end
+  
+  if $brief
+    puts(overallResults.collect{|stats| stats.mean}.join("\t"))
+    puts(overallResults.collect{|stats| stats.confInt}.join("\t"))
+  end
+  
+  
+end
+
+begin
+  $sawBenchOptions = false
+  
+  def resetBenchOptionsIfNecessary
+    unless $sawBenchOptions
+      $includeSunSpider = false
+      $includeV8 = false
+      $includeKraken = false
+      $sawBenchOptions = true
+    end
+  end
+  
+  GetoptLong.new(['--rerun', GetoptLong::REQUIRED_ARGUMENT],
+                 ['--inner', GetoptLong::REQUIRED_ARGUMENT],
+                 ['--outer', GetoptLong::REQUIRED_ARGUMENT],
+                 ['--warmup', GetoptLong::REQUIRED_ARGUMENT],
+                 ['--timing-mode', GetoptLong::REQUIRED_ARGUMENT],
+                 ['--sunspider-only', GetoptLong::NO_ARGUMENT],
+                 ['--v8-only', GetoptLong::NO_ARGUMENT],
+                 ['--kraken-only', GetoptLong::NO_ARGUMENT],
+                 ['--exclude-sunspider', GetoptLong::NO_ARGUMENT],
+                 ['--exclude-v8', GetoptLong::NO_ARGUMENT],
+                 ['--exclude-kraken', GetoptLong::NO_ARGUMENT],
+                 ['--sunspider', GetoptLong::NO_ARGUMENT],
+                 ['--v8', GetoptLong::NO_ARGUMENT],
+                 ['--kraken', GetoptLong::NO_ARGUMENT],
+                 ['--benchmarks', GetoptLong::REQUIRED_ARGUMENT],
+                 ['--measure-gc', GetoptLong::OPTIONAL_ARGUMENT],
+                 ['--force-vm-kind', GetoptLong::REQUIRED_ARGUMENT],
+                 ['--force-vm-copy', GetoptLong::NO_ARGUMENT],
+                 ['--dont-copy-vms', GetoptLong::NO_ARGUMENT],
+                 ['--verbose', '-v', GetoptLong::NO_ARGUMENT],
+                 ['--brief', GetoptLong::NO_ARGUMENT],
+                 ['--silent', GetoptLong::NO_ARGUMENT],
+                 ['--remote', GetoptLong::REQUIRED_ARGUMENT],
+                 ['--local', GetoptLong::NO_ARGUMENT],
+                 ['--ssh-options', GetoptLong::REQUIRED_ARGUMENT],
+                 ['--slave', GetoptLong::NO_ARGUMENT],
+                 ['--prepare-only', GetoptLong::NO_ARGUMENT],
+                 ['--analyze', GetoptLong::REQUIRED_ARGUMENT],
+                 ['--vms', GetoptLong::REQUIRED_ARGUMENT],
+                 ['--help', '-h', GetoptLong::NO_ARGUMENT]).each {
+    | opt, arg |
+    case opt
+    when '--rerun'
+      $rerun = intArg(opt,arg,1,nil)
+    when '--inner'
+      $inner = intArg(opt,arg,1,nil)
+    when '--outer'
+      $outer = intArg(opt,arg,1,nil)
+    when '--warmup'
+      $warmup = intArg(opt,arg,0,nil)
+    when '--timing-mode'
+      if arg.upcase == "PRECISETIME"
+        $timeMode = :preciseTime
+      elsif arg.upcase == "DATE"
+        $timeMode = :date
+      elsif arg.upcase == "AUTO"
+        $timeMode = :auto
+      else
+        quickFail("Expected either 'preciseTime', 'date', or 'auto' for --time-mode, but got '#{arg}'.",
+                  "Invalid argument for command-line option")
+      end
+    when '--force-vm-kind'
+      if arg.upcase == "JSC"
+        $forceVMKind = :jsc
+      elsif arg.upcase == "DUMPRENDERTREE"
+        $forceVMKind = :dumpRenderTree
+      elsif arg.upcase == "AUTO"
+        $forceVMKind = nil
+      else
+        quickFail("Expected either 'jsc' or 'DumpRenderTree' for --force-vm-kind, but got '#{arg}'.",
+                  "Invalid argument for command-line option")
+      end
+    when '--force-vm-copy'
+      $needToCopyVMs = true
+    when '--dont-copy-vms'
+      $dontCopyVMs = true
+    when '--sunspider-only'
+      $includeV8 = false
+      $includeKraken = false
+    when '--v8-only'
+      $includeSunSpider = false
+      $includeKraken = false
+    when '--kraken-only'
+      $includeSunSpider = false
+      $includeV8 = false
+    when '--exclude-sunspider'
+      $includeSunSpider = false
+    when '--exclude-v8'
+      $includeV8 = false
+    when '--exclude-kraken'
+      $includeKraken = false
+    when '--sunspider'
+      resetBenchOptionsIfNecessary
+      $includeSunSpider = true
+    when '--v8'
+      resetBenchOptionsIfNecessary
+      $includeV8 = true
+    when '--kraken'
+      resetBenchOptionsIfNecessary
+      $includeKraken = true
+    when '--benchmarks'
+      $benchmarkPattern = Regexp.new(arg)
+    when '--measure-gc'
+      if arg == ''
+        $measureGC = true
+      else
+        $measureGC = arg
+      end
+    when '--verbose'
+      $verbosity += 1
+    when '--brief'
+      $brief = true
+    when '--silent'
+      $silent = true
+    when '--remote'
+      $remoteHosts += arg.split(',')
+      $needToCopyVMs = true
+    when '--ssh-options'
+      $sshOptions << arg
+    when '--local'
+      $alsoLocal = true
+    when '--prepare-only'
+      $run = false
+    when '--analyze'
+      $prepare = false
+      $run = false
+      $analyze << arg
+    when '--help'
+      usage
+    else
+      raise "bad option: #{opt}"
+    end
+  }
+  
+  # If the --dont-copy-vms option was passed, it overrides the --force-vm-copy option.
+  if $dontCopyVMs
+    $needToCopyVMs = false
+  end
+  
+  SUNSPIDER = BenchmarkSuite.new("SunSpider", SUNSPIDER_PATH, :arithmeticMean)
+  ["3d-cube", "3d-morph", "3d-raytrace", "access-binary-trees",
+   "access-fannkuch", "access-nbody", "access-nsieve",
+   "bitops-3bit-bits-in-byte", "bitops-bits-in-byte", "bitops-bitwise-and",
+   "bitops-nsieve-bits", "controlflow-recursive", "crypto-aes",
+   "crypto-md5", "crypto-sha1", "date-format-tofte", "date-format-xparb",
+   "math-cordic", "math-partial-sums", "math-spectral-norm", "regexp-dna",
+   "string-base64", "string-fasta", "string-tagcloud",
+   "string-unpack-code", "string-validate-input"].each {
+    | name |
+    SUNSPIDER.add SunSpiderBenchmark.new(name)
+  }
+
+  V8 = BenchmarkSuite.new("V8", V8_PATH, :geometricMean)
+  ["crypto", "deltablue", "earley-boyer", "raytrace",
+   "regexp", "richards", "splay"].each {
+    | name |
+    V8.add V8Benchmark.new(name)
+  }
+
+  KRAKEN = BenchmarkSuite.new("Kraken", KRAKEN_PATH, :arithmeticMean)
+  ["ai-astar", "audio-beat-detection", "audio-dft", "audio-fft",
+   "audio-oscillator", "imaging-darkroom", "imaging-desaturate",
+   "imaging-gaussian-blur", "json-parse-financial",
+   "json-stringify-tinderbox", "stanford-crypto-aes",
+   "stanford-crypto-ccm", "stanford-crypto-pbkdf2",
+   "stanford-crypto-sha256-iterative"].each {
+    | name |
+    KRAKEN.add KrakenBenchmark.new(name)
+  }
+
+  ARGV.each {
+    | vm |
+    if vm =~ /([a-zA-Z0-9_ ]+):/
+      name = $1
+      nameKind = :given
+      vm = $~.post_match
+    else
+      name = "Conf\##{$vms.length+1}"
+      nameKind = :auto
+    end
+    $stderr.puts "#{name}: #{vm}" if $verbosity >= 1
+    $vms << VM.new(Pathname.new(vm).realpath, name, nameKind, nil)
+  }
+  
+  if $vms.empty?
+    quickFail("Please specify at least on configuraiton on the command line.",
+              "Insufficient arguments")
+  end
+  
+  $vms.each {
+    | vm |
+    if vm.vmType != :jsc and $timeMode != :date
+      $timeMode = :date
+      $stderr.puts "Warning: using Date.now() instead of preciseTime() because #{vm} doesn't support the latter."
+    end
+  }
+  
+  if FileTest.exist? BENCH_DATA_PATH
+    cmd = "rm -rf #{BENCH_DATA_PATH}"
+    $stderr.puts ">> #{cmd}" if $verbosity >= 2
+    raise unless system cmd
+  end
+  
+  Dir.mkdir BENCH_DATA_PATH
+  
+  if $needToCopyVMs
+    canCopyIntoBenchPath = true
+    $vms.each {
+      | vm |
+      canCopyIntoBenchPath = false unless vm.canCopyIntoBenchPath
+    }
+    
+    if canCopyIntoBenchPath
+      $vms.each {
+        | vm |
+        $stderr.puts "Copying #{vm} into #{BENCH_DATA_PATH}..."
+        vm.copyIntoBenchPath
+      }
+      $stderr.puts "All VMs are in place."
+    else
+      $stderr.puts "Warning: don't know how to copy some VMs into #{BENCH_DATA_PATH}, so I won't do it."
+    end
+  end
+  
+  if $measureGC and $measureGC != true
+    found = false
+    $vms.each {
+      | vm |
+      if vm.name == $measureGC
+        found = true
+      end
+    }
+    unless found
+      $stderr.puts "Warning: --measure-gc option ignored because no VM is named #{$measureGC}"
+    end
+  end
+  
+  if $outer*$inner == 1
+    $stderr.puts "Warning: will only collect one sample per benchmark/VM.  Confidence interval calculation will fail."
+  end
+  
+  $stderr.puts "Using timeMode = #{$timeMode}." if $verbosity >= 1
+  
+  $suites = []
+  
+  if $includeSunSpider and not SUNSPIDER.empty?
+    $suites << SUNSPIDER
+  end
+  
+  if $includeV8 and not V8.empty?
+    $suites << V8
+  end
+  
+  if $includeKraken and not KRAKEN.empty?
+    $suites << KRAKEN
+  end
+  
+  $benchmarks = []
+  $suites.each {
+    | suite |
+    $benchmarks += suite.benchmarks
+  }
+  
+  $runPlans = []
+  $vms.each {
+    | vm |
+    $benchmarks.each {
+      | benchmark |
+      $outer.times {
+        | iteration |
+        $runPlans << BenchRunPlan.new(benchmark, vm, iteration)
+      }
+    }
+  }
+  
+  $runPlans.shuffle!
+  
+  $suitepad = $suites.collect {
+    | suite |
+    suite.to_s.size
+  }.max + 1
+  
+  $benchpad = ($benchmarks +
+               ["<arithmetic> *", "<geometric> *", "<harmonic> *"]).collect {
+    | benchmark |
+    if benchmark.respond_to? :name
+      benchmark.name.size
+    else
+      benchmark.size
+    end
+  }.max + 1
+
+  $vmpad = $vms.collect {
+    | vm |
+    vm.to_s.size
+  }.max + 1
+  
+  if $prepare
+    File.open("#{BENCH_DATA_PATH}/runscript", "w") {
+      | file |
+      file.puts "echo \"HOSTNAME:\\c\""
+      file.puts "hostname"
+      file.puts "echo"
+      file.puts "echo \"HARDWARE:\\c\""
+      file.puts "/usr/sbin/sysctl hw.model"
+      file.puts "echo"
+      file.puts "set -e"
+      $script = file
+      $runPlans.each_with_index {
+        | plan, idx |
+        if $verbosity == 0 and not $silent
+          text1 = lpad(idx.to_s,$runPlans.size.to_s.size)+"/"+$runPlans.size.to_s
+          text2 = plan.benchmark.to_s+"/"+plan.vm.to_s
+          file.puts("echo "+("\r#{text1} #{rpad(text2,$suitepad+1+$benchpad+1+$vmpad)}".inspect)[0..-2]+"\\c\" 1>&2")
+          file.puts("echo "+("\r#{text1} #{text2}".inspect)[0..-2]+"\\c\" 1>&2")
+        end
+        plan.emitRunCode
+      }
+      if $verbosity == 0 and not $silent
+        file.puts("echo "+("\r#{$runPlans.size}/#{$runPlans.size} #{' '*($suitepad+1+$benchpad+1+$vmpad)}".inspect)[0..-2]+"\\c\" 1>&2")
+        file.puts("echo "+("\r#{$runPlans.size}/#{$runPlans.size}".inspect)+" 1>&2")
+      end
+    }
+  end
+  
+  if $run
+    unless $remoteHosts.empty?
+      $stderr.puts "Packaging benchmarking directory for remote hosts..." if $verbosity==0
+      Dir.chdir(TEMP_PATH) {
+        cmd = "tar -czf payload.tar.gz benchdata"
+        $stderr.puts ">> #{cmd}" if $verbosity>=2
+        raise unless system(cmd)
+      }
+      
+      def grokHost(host)
+        if host =~ /:([0-9]+)$/
+          "-p " + $1 + " " + $~.pre_match.inspect
+        else
+          host.inspect
+        end
+      end
+      
+      def sshRead(host, command)
+        cmd = "ssh #{$sshOptions.collect{|x| x.inspect}.join(' ')} #{grokHost(host)} #{command.inspect}"
+        $stderr.puts ">> #{cmd}" if $verbosity>=2
+        result = ""
+        IO.popen(cmd, "r") {
+          | inp |
+          inp.each_line {
+            | line |
+            $stderr.puts "#{host}: #{line}" if $verbosity>=2
+            result += line
+          }
+        }
+        raise "#{$?}" unless $?.success?
+        result
+      end
+      
+      def sshWrite(host, command, data)
+        cmd = "ssh #{$sshOptions.collect{|x| x.inspect}.join(' ')} #{grokHost(host)} #{command.inspect}"
+        $stderr.puts ">> #{cmd}" if $verbosity>=2
+        IO.popen(cmd, "w") {
+          | outp |
+          outp.write(data)
+        }
+        raise "#{$?}" unless $?.success?
+      end
+      
+      $remoteHosts.each {
+        | host |
+        $stderr.puts "Sending benchmark payload to #{host}..." if $verbosity==0
+        
+        remoteTempPath = JSON::parse(sshRead(host, "cat ~/.bencher"))["tempPath"]
+        raise unless remoteTempPath
+        
+        sshWrite(host, "cd #{remoteTempPath.inspect} && rm -rf benchdata && tar -xz", IO::read("#{TEMP_PATH}/payload.tar.gz"))
+        
+        $stderr.puts "Running on #{host}..." if $verbosity==0
+        
+        parseAndDisplayResults(sshRead(host, "cd #{(remoteTempPath+'/benchdata').inspect} && sh runscript"))
+      }
+    end
+    
+    if not $remoteHosts.empty? and $alsoLocal
+      $stderr.puts "Running locally..."
+    end
+    
+    if $remoteHosts.empty? or $alsoLocal
+      parseAndDisplayResults(runAndGetResults)
+    end
+  end
+  
+  $analyze.each_with_index {
+    | filename, index |
+    if index >= 1
+      puts
+    end
+    parseAndDisplayResults(IO::read(filename))
+  }
+  
+  if $prepare and not $run and $analyze.empty?
+    puts wrap("Benchmarking script and data are in #{BENCH_DATA_PATH}. You can run "+
+              "the benchmarks and get the results by doing:", 78)
+    puts
+    puts "cd #{BENCH_DATA_PATH}"
+    puts "sh runscript > results.txt"
+    puts
+    puts wrap("Then you can analyze the results by running bencher with the same arguments "+
+              "as now, but replacing --prepare-only with --analyze results.txt.", 78)
+  end
+rescue => e
+  fail(e)
+end
+  
+  
diff --git a/Tools/Scripts/bisect-builds b/Tools/Scripts/bisect-builds
new file mode 100755
index 0000000..b970db3
--- /dev/null
+++ b/Tools/Scripts/bisect-builds
@@ -0,0 +1,445 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2007, 2008, 2011 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.
+
+# This script attempts to find the point at which a regression (or progression)
+# of behavior occurred by searching WebKit nightly builds.
+
+# To override the location where the nightly builds are downloaded or the path
+# to the Safari web browser, create a ~/.bisect-buildsrc file with one or more of
+# the following lines (use "~/" to specify a path from your home directory):
+#
+# $branch = "branch-name";
+# $nightlyDownloadDirectory = "~/path/to/nightly/downloads";
+# $safariPath = "/path/to/Safari.app";
+
+use strict;
+
+use File::Basename;
+use File::Path;
+use File::Spec;
+use File::Temp qw(tempfile);
+use FindBin;
+use Getopt::Long;
+use Time::HiRes qw(usleep);
+
+use lib $FindBin::Bin;
+use webkitdirs qw(safariPathFromSafariBundle);
+
+sub createTempFile($);
+sub downloadNightly($$$);
+sub findMacOSXVersion();
+sub findNearestNightlyIndex(\@$$);
+sub findSafariVersion($);
+sub loadSettings();
+sub makeNightlyList($$$$);
+sub max($$) { return $_[0] > $_[1] ? $_[0] : $_[1]; }
+sub mountAndRunNightly($$$$);
+sub parseRevisions($$;$);
+sub printStatus($$$);
+sub printTracLink($$);
+sub promptForTest($);
+
+loadSettings();
+
+my %validBranches = map { $_ => 1 } qw(feature-branch trunk);
+my $branch = $Settings::branch;
+my $nightlyDownloadDirectory = $Settings::nightlyDownloadDirectory;
+my $safariPath = $Settings::safariPath;
+
+my @nightlies;
+
+my $isProgression;
+my $localOnly;
+my @revisions;
+my $sanityCheck;
+my $showHelp;
+my $testURL;
+
+# Fix up -r switches in @ARGV
+@ARGV = map { /^(-r)(.+)$/ ? ($1, $2) : $_ } @ARGV;
+
+my $result = GetOptions(
+    "b|branch=s"             => \$branch,
+    "d|download-directory=s" => \$nightlyDownloadDirectory,
+    "h|help"                 => \$showHelp,
+    "l|local!"               => \$localOnly,
+    "p|progression!"         => \$isProgression,
+    "r|revisions=s"          => \&parseRevisions,
+    "safari-path=s"          => \$safariPath,
+    "s|sanity-check!"        => \$sanityCheck,
+);
+$testURL = shift @ARGV;
+
+$branch = "feature-branch" if $branch eq "feature";
+if (!exists $validBranches{$branch}) {
+    print STDERR "ERROR: Invalid branch '$branch'\n";
+    $showHelp = 1;
+}
+
+if (!$result || $showHelp || scalar(@ARGV) > 0) {
+    print STDERR "Search WebKit nightly builds for changes in behavior.\n";
+    print STDERR "Usage: " . basename($0) . " [options] [url]\n";
+    print STDERR <<END;
+  [-b|--branch name]             name of the nightly build branch (default: trunk)
+  [-d|--download-directory dir]  nightly build download directory (default: ~/Library/Caches/WebKit-Nightlies)
+  [-h|--help]                    show this help message
+  [-l|--local]                   only use local (already downloaded) nightlies
+  [-p|--progression]             searching for a progression, not a regression
+  [-r|--revision M[:N]]          specify starting (and optional ending) revisions to search
+  [--safari-path path]           path to Safari application bundle (default: /Applications/Safari.app)
+  [-s|--sanity-check]            verify both starting and ending revisions before bisecting
+END
+    exit 1;
+}
+
+my $nightlyWebSite = "http://nightly.webkit.org";
+my $nightlyBuildsURLBase = $nightlyWebSite . File::Spec->catdir("/builds", $branch, "mac");
+my $nightlyFilesURLBase = $nightlyWebSite . File::Spec->catdir("/files", $branch, "mac");
+
+$nightlyDownloadDirectory = glob($nightlyDownloadDirectory) if $nightlyDownloadDirectory =~ /^~/;
+$safariPath = glob($safariPath) if $safariPath =~ /^~/;
+$safariPath = safariPathFromSafariBundle($safariPath) if $safariPath =~ m#\.app/*#;
+
+$nightlyDownloadDirectory = File::Spec->catdir($nightlyDownloadDirectory, $branch);
+if (! -d $nightlyDownloadDirectory) {
+    mkpath($nightlyDownloadDirectory, 0, 0755) || die "Could not create $nightlyDownloadDirectory: $!";
+}
+
+@nightlies = makeNightlyList($localOnly, $nightlyDownloadDirectory, findMacOSXVersion(), findSafariVersion($safariPath));
+
+my $startIndex = $revisions[0] ? findNearestNightlyIndex(@nightlies, $revisions[0], 'ceil') : 0;
+my $endIndex = $revisions[1] ? findNearestNightlyIndex(@nightlies, $revisions[1], 'floor') : $#nightlies;
+
+my $tempFile = createTempFile($testURL);
+
+if ($sanityCheck) {
+    my $didReproduceBug;
+
+    do {
+        printf "\nChecking starting revision r%s...\n",
+            $nightlies[$startIndex]->{rev};
+        downloadNightly($nightlies[$startIndex]->{file}, $nightlyFilesURLBase, $nightlyDownloadDirectory);
+        mountAndRunNightly($nightlies[$startIndex]->{file}, $nightlyDownloadDirectory, $safariPath, $tempFile);
+        $didReproduceBug = promptForTest($nightlies[$startIndex]->{rev});
+        $startIndex-- if $didReproduceBug < 0;
+    } while ($didReproduceBug < 0);
+    die "ERROR: Bug reproduced in starting revision!  Do you need to test an earlier revision or for a progression?"
+        if $didReproduceBug && !$isProgression;
+    die "ERROR: Bug not reproduced in starting revision!  Do you need to test an earlier revision or for a regression?"
+        if !$didReproduceBug && $isProgression;
+
+    do {
+        printf "\nChecking ending revision r%s...\n",
+            $nightlies[$endIndex]->{rev};
+        downloadNightly($nightlies[$endIndex]->{file}, $nightlyFilesURLBase, $nightlyDownloadDirectory);
+        mountAndRunNightly($nightlies[$endIndex]->{file}, $nightlyDownloadDirectory, $safariPath, $tempFile);
+        $didReproduceBug = promptForTest($nightlies[$endIndex]->{rev});
+        $endIndex++ if $didReproduceBug < 0;
+    } while ($didReproduceBug < 0);
+    die "ERROR: Bug NOT reproduced in ending revision!  Do you need to test a later revision or for a progression?"
+        if !$didReproduceBug && !$isProgression;
+    die "ERROR: Bug reproduced in ending revision!  Do you need to test a later revision or for a regression?"
+        if $didReproduceBug && $isProgression;
+}
+
+printStatus($nightlies[$startIndex]->{rev}, $nightlies[$endIndex]->{rev}, $isProgression);
+
+my %brokenRevisions = ();
+while (abs($endIndex - $startIndex) > 1) {
+    my $index = $startIndex + int(($endIndex - $startIndex) / 2);
+
+    my $didReproduceBug;
+    do {
+        if (exists $nightlies[$index]) {
+            my $buildsLeft = max(max(0, $endIndex - $index - 1), max(0, $index - $startIndex - 1));
+            my $plural = $buildsLeft == 1 ? "" : "s";
+            printf "\nChecking revision r%s (%d build%s left to test after this)...\n", $nightlies[$index]->{rev}, $buildsLeft, $plural;
+            downloadNightly($nightlies[$index]->{file}, $nightlyFilesURLBase, $nightlyDownloadDirectory);
+            mountAndRunNightly($nightlies[$index]->{file}, $nightlyDownloadDirectory, $safariPath, $tempFile);
+            $didReproduceBug = promptForTest($nightlies[$index]->{rev});
+        }
+        if ($didReproduceBug < 0) {
+            $brokenRevisions{$nightlies[$index]->{rev}} = $nightlies[$index]->{file};
+            delete $nightlies[$index];
+            $endIndex--;
+            $index = $startIndex + int(($endIndex - $startIndex) / 2);
+        }
+    } while ($didReproduceBug < 0);
+
+    if ($didReproduceBug && !$isProgression || !$didReproduceBug && $isProgression) {
+        $endIndex = $index;
+    } else {
+        $startIndex = $index;
+    }
+
+    print "\nBroken revisions skipped: r" . join(", r", keys %brokenRevisions) . "\n"
+        if scalar keys %brokenRevisions > 0;
+    printStatus($nightlies[$startIndex]->{rev}, $nightlies[$endIndex]->{rev}, $isProgression);
+}
+
+printTracLink($nightlies[$startIndex]->{rev}, $nightlies[$endIndex]->{rev});
+
+unlink $tempFile if $tempFile;
+
+exit 0;
+
+sub createTempFile($)
+{
+    my ($url) = @_;
+
+    return undef if !$url;
+
+    my ($fh, $tempFile) = tempfile(
+        basename($0) . "-XXXXXXXX",
+        DIR => File::Spec->tmpdir(),
+        SUFFIX => ".html",
+        UNLINK => 0,
+    );
+    print $fh "<meta http-equiv=\"refresh\" content=\"0; $url\">\n";
+    close($fh);
+
+    return $tempFile;
+}
+
+sub downloadNightly($$$)
+{
+    my ($filename, $urlBase, $directory) = @_;
+    my $path = File::Spec->catfile($directory, $filename);
+    if (! -f $path) {
+        print "Downloading $filename to $directory...\n";
+        `curl -# -o '$path' '$urlBase/$filename'`;
+    }
+}
+
+sub findMacOSXVersion()
+{
+    my $version;
+    open(SW_VERS, "-|", "/usr/bin/sw_vers") || die;
+    while (<SW_VERS>) {
+        $version = $1 if /^ProductVersion:\s+([^\s]+)/;
+    }
+    close(SW_VERS);
+    return $version;
+}
+
+sub findNearestNightlyIndex(\@$$)
+{
+    my ($nightlies, $revision, $round) = @_;
+
+    my $lowIndex = 0;
+    my $highIndex = $#{$nightlies};
+
+    return $highIndex if uc($revision) eq 'HEAD' || $revision >= $nightlies->[$highIndex]->{rev};
+    return $lowIndex if $revision <= $nightlies->[$lowIndex]->{rev};
+
+    while (abs($highIndex - $lowIndex) > 1) {
+        my $index = $lowIndex + int(($highIndex - $lowIndex) / 2);
+        if ($revision < $nightlies->[$index]->{rev}) {
+            $highIndex = $index;
+        } elsif ($revision > $nightlies->[$index]->{rev}) {
+            $lowIndex = $index;
+        } else {
+            return $index;
+        }
+    }
+
+    return ($round eq "floor") ? $lowIndex : $highIndex;
+}
+
+sub findSafariVersion($)
+{
+    my ($path) = @_;
+    my $versionPlist = File::Spec->catdir(dirname(dirname($path)), "version.plist");
+    my $version;
+    open(PLIST, "< $versionPlist") || die;
+    while (<PLIST>) {
+        if (m#^\s*<key>CFBundleShortVersionString</key>#) {
+            $version = <PLIST>;
+            $version =~ s#^\s*<string>([0-9.]+)[^<]*</string>\s*[\r\n]*#$1#;
+        }
+    }
+    close(PLIST);
+    return $version;
+}
+
+sub loadSettings()
+{
+    package Settings;
+
+    our $branch = "trunk";
+    our $nightlyDownloadDirectory = File::Spec->catdir($ENV{HOME}, "Library/Caches/WebKit-Nightlies");
+    our $safariPath = "/Applications/Safari.app";
+
+    my $rcfile = File::Spec->catdir($ENV{HOME}, ".bisect-buildsrc");
+    return if !-f $rcfile;
+
+    my $result = do $rcfile;
+    die "Could not parse $rcfile: $@" if $@;
+}
+
+sub makeNightlyList($$$$)
+{
+    my ($useLocalFiles, $localDirectory, $macOSXVersion, $safariVersion) = @_;
+    my @files;
+
+    if ($useLocalFiles) {
+        opendir(DIR, $localDirectory) || die "$!";
+        foreach my $file (readdir(DIR)) {
+            if ($file =~ /^WebKit-SVN-r([0-9]+)\.dmg$/) {
+                push(@files, +{ rev => $1, file => $file });
+            }
+        }
+        closedir(DIR);
+    } else {
+        open(NIGHTLIES, "curl -s $nightlyBuildsURLBase/all |") || die;
+
+        while (my $line = <NIGHTLIES>) {
+            chomp $line;
+            my ($revision, $timestamp, $url) = split(/,/, $line);
+            my $nightly = basename($url);
+            push(@files, +{ rev => $revision, file => $nightly });
+        }
+        close(NIGHTLIES);
+    }
+
+    if (eval "v$macOSXVersion" ge v10.5) {
+        if ($safariVersion eq "4 Public Beta") {
+            @files = grep { $_->{rev} >= 39682 } @files;
+        } elsif (eval "v$safariVersion" ge v3.2) {
+            @files = grep { $_->{rev} >= 37348 } @files;
+        } elsif (eval "v$safariVersion" ge v3.1) {
+            @files = grep { $_->{rev} >= 29711 } @files;
+        } elsif (eval "v$safariVersion" ge v3.0) {
+            @files = grep { $_->{rev} >= 25124 } @files;
+        } elsif (eval "v$safariVersion" ge v2.0) {
+            @files = grep { $_->{rev} >= 19594 } @files;
+        } else {
+            die "Requires Safari 2.0 or newer";
+        }
+    } elsif (eval "v$macOSXVersion" ge v10.4) {
+        if ($safariVersion eq "4 Public Beta") {
+            @files = grep { $_->{rev} >= 39682 } @files;
+        } elsif (eval "v$safariVersion" ge v3.2) {
+            @files = grep { $_->{rev} >= 37348 } @files;
+        } elsif (eval "v$safariVersion" ge v3.1) {
+            @files = grep { $_->{rev} >= 29711 } @files;
+        } elsif (eval "v$safariVersion" ge v3.0) {
+            @files = grep { $_->{rev} >= 19992 } @files;
+        } elsif (eval "v$safariVersion" ge v2.0) {
+            @files = grep { $_->{rev} >= 11976 } @files;
+        } else {
+            die "Requires Safari 2.0 or newer";
+        }
+    } else {
+        die "Requires Mac OS X 10.4 (Tiger) or 10.5 (Leopard)";
+    }
+
+    my $nightlycmp = sub { return $a->{rev} <=> $b->{rev}; };
+
+    return sort $nightlycmp @files;
+}
+
+sub mountAndRunNightly($$$$)
+{
+    my ($filename, $directory, $safari, $tempFile) = @_;
+    my $mountPath = "/Volumes/WebKit";
+    my $webkitApp = File::Spec->catfile($mountPath, "WebKit.app");
+    my $diskImage = File::Spec->catfile($directory, $filename);
+    my $devNull = File::Spec->devnull();
+
+    my $i = 0;
+    while (-e $mountPath) {
+        $i++;
+        usleep 100 if $i > 1;
+        `hdiutil detach '$mountPath' 2> $devNull`;
+        die "Could not unmount $diskImage at $mountPath" if $i > 100;
+    }
+    die "Can't mount $diskImage: $mountPath already exists!" if -e $mountPath;
+
+    print "Mounting disk image and running WebKit...\n";
+    `hdiutil attach '$diskImage'`;
+    $i = 0;
+    while (! -e $webkitApp) {
+        usleep 100;
+        $i++;
+        die "Could not mount $diskImage at $mountPath" if $i > 100;
+    }
+
+    my $frameworkPath;
+    if (-d "/Volumes/WebKit/WebKit.app/Contents/Frameworks") {
+        my $osXVersion = join('.', (split(/\./, findMacOSXVersion()))[0..1]);
+        $frameworkPath = "/Volumes/WebKit/WebKit.app/Contents/Frameworks/$osXVersion";
+    } else {
+        $frameworkPath = "/Volumes/WebKit/WebKit.app/Contents/Resources";
+    }
+
+    $tempFile ||= "";
+    `DYLD_FRAMEWORK_PATH=$frameworkPath WEBKIT_UNSET_DYLD_FRAMEWORK_PATH=YES $safari $tempFile`;
+
+    `hdiutil detach '$mountPath' 2> $devNull`;
+}
+
+sub parseRevisions($$;$)
+{
+    my ($optionName, $value, $ignored) = @_;
+
+    if ($value =~ /^r?([0-9]+|HEAD):?$/i) {
+        push(@revisions, $1);
+        die "Too many revision arguments specified" if scalar @revisions > 2;
+    } elsif ($value =~ /^r?([0-9]+):?r?([0-9]+|HEAD)$/i) {
+        $revisions[0] = $1;
+        $revisions[1] = $2;
+    } else {
+        die "Unknown revision '$value':  expected 'M' or 'M:N'";
+    }
+}
+
+sub printStatus($$$)
+{
+    my ($startRevision, $endRevision, $isProgression) = @_;
+    printf "\n%s: r%s  %s: r%s\n",
+        $isProgression ? "Fails" : "Works", $startRevision,
+        $isProgression ? "Works" : "Fails", $endRevision;
+}
+
+sub printTracLink($$)
+{
+    my ($startRevision, $endRevision) = @_;
+    printf("http://trac.webkit.org/log/trunk/?rev=%s&stop_rev=%s\n", $endRevision, $startRevision + 1);
+}
+
+sub promptForTest($)
+{
+    my ($revision) = @_;
+    print "Did the bug reproduce in r$revision (yes/no/broken)? ";
+    my $answer = <STDIN>;
+    return 1 if $answer =~ /^(1|y.*)$/i;
+    return -1 if $answer =~ /^(-1|b.*)$/i; # Broken
+    return 0;
+}
+
diff --git a/Tools/Scripts/build-api-tests b/Tools/Scripts/build-api-tests
new file mode 100755
index 0000000..09d19bf
--- /dev/null
+++ b/Tools/Scripts/build-api-tests
@@ -0,0 +1,76 @@
+#!/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
+
+my $result;
+if (isAppleMacWebKit()) {
+    chdir "Source/ThirdParty/gtest";
+    buildXCodeProject("xcode/gtest", $clean, XcodeOptions(), @ARGV);
+    chdir "../../../Tools/TestWebKitAPI" or die;
+    $result = buildXCodeProject("TestWebKitAPI", $clean, XcodeOptions(), @ARGV);
+} elsif (isAppleWinWebKit()) {
+    chdir "Tools/TestWebKitAPI" or die;
+    $result = buildVisualStudioProject("win/TestWebKitAPI.sln", $clean);
+} elsif (isChromium()) {
+    # Chromium build everything in one shot. No need to build anything here.
+    $result = 0;
+} else {
+    die "TestWebKitAPI is not supported on this platform.\n";
+}
+
+exit exitStatus($result);
diff --git a/Tools/Scripts/build-dumprendertree b/Tools/Scripts/build-dumprendertree
new file mode 100755
index 0000000..4e206f4
--- /dev/null
+++ b/Tools/Scripts/build-dumprendertree
@@ -0,0 +1,81 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005, 2006, 2007, 2008, 2009 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.
+
+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
+  --gtk         Build the GTK+ port
+  --qt          Build the Qt port
+  --wx          Build the wxWindows port
+  --chromium    Build the Chromium port
+  --efl         Build the EFL port
+EOF
+
+GetOptions(
+    'help' => \$showHelp,
+    'clean' => \$clean,
+);
+
+if ($showHelp) {
+   print STDERR $usage;
+   exit 1;
+}
+
+checkRequiredSystemConfig();
+setConfiguration();
+chdirWebKit();
+
+# Build
+chdir "Tools/DumpRenderTree" or die;
+
+my $result;
+if (isAppleMacWebKit()) {
+    $result = buildXCodeProject("DumpRenderTree", $clean, XcodeOptions(), @ARGV);
+} elsif (isAppleWinWebKit()) {
+    $result = buildVisualStudioProject("DumpRenderTree.sln", $clean);
+} elsif (isQt() || isGtk() || isWx() || isChromium() || isEfl()) {
+    # Qt, Gtk wxWindows, Chromium and EFL build everything in one shot. No need to build anything here.
+    $result = 0;
+} else {
+    die "Building not defined for this platform!\n";
+}
+
+exit exitStatus($result);
diff --git a/Tools/Scripts/build-jsc b/Tools/Scripts/build-jsc
new file mode 100755
index 0000000..3fbf43f
--- /dev/null
+++ b/Tools/Scripts/build-jsc
@@ -0,0 +1,100 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005 Apple Computer, Inc.  All rights reserved.
+# Copyright (C) 2007 Eric Seidel <eric@webkit.org>
+#
+# 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.
+
+
+use strict;
+use FindBin;
+use Getopt::Long qw(:config pass_through);
+use lib $FindBin::Bin;
+use webkitdirs;
+use POSIX;
+
+my $coverageSupport = 0;
+my $showHelp = 0;
+
+my $programName = basename($0);
+my $usage = <<EOF;
+Usage: $programName [options] [options to pass to build system]
+  --help                        Show this help message
+  --[no-]coverage               Toggle code coverage support (default: $coverageSupport)
+EOF
+
+GetOptions(
+    'coverage!' => \$coverageSupport,
+    'help' => \$showHelp
+);
+
+if ($showHelp) {
+   print STDERR $usage;
+   exit 1;
+}
+
+checkRequiredSystemConfig();
+setConfiguration();
+chdirWebKit();
+my @options = XcodeOptions();
+my @coverageSupportOptions = ($coverageSupport) ? XcodeCoverageSupportOptions() : ();
+
+if (isQt()) {
+    checkForArgumentAndRemoveFromARGV("--qt");
+    my @projects = ("WTF", "JavaScriptCore");
+    # Pick up the --no-webkit2 option from BUILD_WEBKIT_ARGS if it is needed
+    push @ARGV, split(/ /, $ENV{'BUILD_WEBKIT_ARGS'}) if ($ENV{'BUILD_WEBKIT_ARGS'});
+    push @ARGV, "WEBKIT_CONFIG-=build_webkit2" if checkForArgumentAndRemoveFromARGV("--no-webkit2");
+    my $result = buildQMakeProjects(\@projects, 0, @ARGV);
+    exit exitStatus($result);
+} elsif (cmakeBasedPortName()) {
+    buildCMakeProjectOrExit(0, cmakeBasedPortName(), undef, "jsc", cmakeBasedPortArguments()); # This call only returns if nothing wrong happened
+    exit exitStatus(0);
+}
+
+sub buildMyProject
+{
+    my ($projectDirectory, $projectName) = @_;
+    my $result;
+    chdir $projectDirectory or die "Can't find $projectName directory to build from";
+    if (isAppleMacWebKit()) {
+        $result = system "sh", "-c", ('xcodebuild -project ' . $projectName . '.xcodeproj "$@" | grep -v setenv && exit ${PIPESTATUS[0]}'), "xcodebuild",  @options, @ARGV, @coverageSupportOptions;
+    } elsif (isAppleWinWebKit()) {
+        $result = buildVisualStudioProject("$projectName.vcproj/$projectName.sln");
+    } elsif (isGtk()) {
+        checkForArgumentAndRemoveFromARGV("--gtk");
+        $result = buildGtkProject($projectName, 0, @ARGV);
+    } elsif (isWx()) {
+        # Builds everything in one-shot. No need to build anything here.
+        $result = 0;
+    } else {
+        die "Building not defined for this platform!\n";
+    }
+    exit exitStatus($result) if exitStatus($result);
+    chdirWebKit();
+}
+
+buildMyProject("Source/WTF", "WTF");
+buildMyProject("Source/JavaScriptCore", "JavaScriptCore");
diff --git a/Tools/Scripts/build-webkit b/Tools/Scripts/build-webkit
new file mode 100755
index 0000000..f36abef
--- /dev/null
+++ b/Tools/Scripts/build-webkit
@@ -0,0 +1,460 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012 Apple Inc. All rights reserved.
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2010 moiji-mobile.com All rights reserved.
+# Copyright (C) 2011 Research In Motion Limited. 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.
+
+# Build script wrapper for the WebKit Open Source Project.
+
+use strict;
+use File::Basename;
+use File::Find;
+use File::Spec;
+use FindBin;
+use Getopt::Long qw(:config pass_through);
+use lib $FindBin::Bin;
+use webkitdirs;
+use webkitperl::FeatureList qw(getFeatureOptionList);
+use POSIX;
+
+sub cMakeArgsFromFeatures();
+sub formatBuildTime($);
+sub writeCongrats();
+
+my $originalWorkingDirectory = getcwd();
+chdirWebKit();
+
+my $showHelp = 0;
+my $clean = 0;
+my $useGYP = 0;
+my $minimal = 0;
+my $installHeaders;
+my $installLibs;
+my $prefixPath;
+my $makeArgs = "";
+my $cmakeArgs;
+my $onlyWebKitProject = 0;
+my $noWebKit2 = 0;
+my $coverageSupport = 0;
+my $startTime = time();
+
+my @features = getFeatureOptionList();
+
+# Update defaults from Qt's project file
+if (isQt()) {
+    # Take a sneek peek at the arguments, since we will need the qmake binary early
+    # on to do profile parsing. We also need to know if we're showing the help-text.
+    foreach (@ARGV) {
+        if (/^--qmake=(.*)/) {
+            setQmakeBinaryPath($1);
+        } elsif (/^--help$/) {
+            $showHelp = 1;
+        }
+    }
+
+    my %qtDefaults;
+    if ($showHelp) {
+        %qtDefaults = qtFeatureDefaults();
+    }
+
+    foreach (@features) {
+        $_->{default} = (%qtDefaults ? $qtDefaults{$_->{define}} || 0 : -1);
+    }
+}
+
+# Additional environment parameters
+push @ARGV, split(/ /, $ENV{'BUILD_WEBKIT_ARGS'}) if ($ENV{'BUILD_WEBKIT_ARGS'});
+
+# Initialize values from defaults
+foreach (@ARGV) {
+    if ($_ eq '--minimal') {
+        $minimal = 1;
+    }
+}
+
+# Initialize values from defaults
+foreach (@features) {
+    ${$_->{value}} = ($minimal ? 0 : $_->{default});
+}
+
+my $programName = basename($0);
+my $usage = <<EOF;
+Usage: $programName [options] [options to pass to build system]
+  --help                            Show this help message
+  --clean                           Cleanup the build directory
+  --debug                           Compile in debug mode
+  --gyp                             Use GYP-generated project files
+  --coverage                        Enable Code Coverage support (Mac only)
+
+  --blackberry                      Build the BlackBerry port on Mac/Linux
+  --chromium                        Build the Chromium port on Mac/Win/Linux
+  --chromium-android                Build the Chromium port on Android
+  --efl                             Build the EFL port
+  --gtk                             Build the GTK+ port
+  --qt                              Build the Qt port
+  --wincairo                        Build using Cairo (rather than CoreGraphics) on Windows
+  --wince                           Build the WinCE port
+
+  --inspector-frontend              Copy changes to the inspector front-end files to the build directory
+
+  --install-headers=<path>          Set installation path for the headers (Qt only)
+  --install-libs=<path>             Set installation path for the libraries (Qt only)
+
+  --prefix=<path>                   Set installation prefix to the given path (Gtk/Efl/BlackBerry only)
+  --makeargs=<arguments>            Optional Makefile flags
+  --qmakearg=<arguments>            Optional qmake flags (Qt only, e.g. --qmakearg="CONFIG+=webkit2" to build WebKit2)
+  --cmakearg=<arguments>            Optional CMake flags (e.g. --cmakearg="-DFOO=bar -DCMAKE_PREFIX_PATH=/usr/local")
+
+  --minimal                         No optional features, unless explicitly enabled
+
+  --only-webkit                     Build only the WebKit project
+  --no-webkit2                      Omit WebKit2 code from the build
+
+EOF
+
+my %options = (
+    'help' => \$showHelp,
+    'clean' => \$clean,
+    'gyp' => \$useGYP,
+    'install-headers=s' => \$installHeaders,
+    'install-libs=s' => \$installLibs,
+    'prefix=s' => \$prefixPath,
+    'makeargs=s' => \$makeArgs,
+    'cmakeargs=s' => \$cmakeArgs,
+    'minimal' => \$minimal,
+    'only-webkit' => \$onlyWebKitProject,
+    'no-webkit2' => \$noWebKit2,
+    'coverage' => \$coverageSupport,
+);
+
+# Build usage text and options list from features
+foreach (@features) {
+    my $opt = sprintf("%-35s", "  --[no-]$_->{option}");
+    $usage .= "$opt $_->{desc} (default: $_->{default})\n";
+    $options{"$_->{option}!"} = $_->{value};
+}
+
+GetOptions(%options);
+
+if ($showHelp) {
+   print STDERR $usage;
+   exit 1;
+}
+
+checkRequiredSystemConfig();
+setConfiguration();
+
+my $productDir = productDir();
+
+# Remove 0 byte sized files from productDir after slave lost for Qt buildbots.
+File::Find::find(\&unlinkZeroFiles, $productDir) if (isQt() && -e $productDir);
+
+sub unlinkZeroFiles()
+{
+    my $file = $File::Find::name;
+    # Remove 0 byte sized files, except
+    # - directories (Because they are always 0 byte sized on Windows)
+    # - .d files used for dependency tracking
+    if (! -d $file && ! -s $file && $file !~ m/\.d$/) {
+        unlink $file;
+        print "0 byte sized file removed from build directory: $file\n";
+    }
+}
+
+# Check that all the project directories are there.
+my @projects = ("Source/JavaScriptCore", "Source/WebCore", "Source/WebKit");
+
+# Build WTF as a separate static library on ports which support it.
+splice @projects, 0, 0, "Source/WTF" if isAppleMacWebKit() or isAppleWinWebKit();
+
+for my $dir (@projects) {
+    if (! -d $dir) {
+        die "Error: No $dir directory found. Please do a fresh checkout.\n";
+    }
+}
+
+if (!isQt() && !-d "WebKitLibraries") {
+    die "Error: No WebKitLibraries directory found. Please do a fresh checkout.\n";
+}
+
+# Generate the generate project files from .gyp files
+if ($useGYP) {
+    system("perl", "Tools/Scripts/generate-project-files") == 0 or die "Failed to run generate-project-files";
+}
+
+my @options = ();
+
+# enable autotool options accordingly
+if (isGtk()) {
+    @options = @ARGV;
+    foreach (@features) {
+        push @options, autotoolsFlag(${$_->{value}}, $_->{option});
+    }
+
+    push @options, "--prefix=" . $prefixPath if defined($prefixPath);
+    push @options, "--makeargs=" . $makeArgs if $makeArgs;
+} elsif (isAppleMacWebKit()) {
+    push @options, XcodeOptions();
+
+    sub option($$$)
+    {
+        my ($feature, $isEnabled, $defaultValue) = @_;
+        return "" if $defaultValue == $isEnabled;
+        return $feature . "=" . ($isEnabled ? $feature : "");
+    }
+
+    foreach (@features) {
+        my $option = option($_->{define}, ${$_->{value}}, $_->{default});
+        push @options, $option unless $option eq "";
+    }
+
+    # ANGLE must come before WebCore
+    splice @projects, 0, 0, "Source/ThirdParty/ANGLE";
+
+    # WebKit2 is only supported in SnowLeopard and later at present.
+    push @projects, ("Source/WebKit2", "Tools/MiniBrowser") if osXVersion()->{"minor"} >= 6 and !$noWebKit2;
+
+    # Build Tools needed for Apple ports
+    push @projects, ("Tools/DumpRenderTree", "Tools/WebKitTestRunner", "Source/ThirdParty/gtest", "Tools/TestWebKitAPI");
+    
+    # Copy library and header from WebKitLibraries to a findable place in the product directory.
+    (system("perl", "Tools/Scripts/copy-webkitlibraries-to-product-directory", $productDir) == 0) or die;
+} elsif (isWinCairo()) {
+    (system("perl Tools/Scripts/update-webkit-wincairo-libs") == 0) or die;
+} elsif (isAppleWinWebKit()) {
+    # Copy WebKitSupportLibrary to the correct location in WebKitLibraries so it can be found.
+    # Will fail if WebKitSupportLibrary.zip is not in source root.
+    (system("perl Tools/Scripts/update-webkit-support-libs") == 0) or die;
+} elsif (isQt()) {
+    push @options, "--install-headers=" . $installHeaders if defined($installHeaders);
+    push @options, "--install-libs=" . $installLibs if defined($installLibs);
+    push @options, "--makeargs=" . $makeArgs if $makeArgs;
+    push @options, "WEBKIT_CONFIG-=build_webkit2" if $noWebKit2;
+
+    if (checkForArgumentAndRemoveFromARGV("-2")) {
+        print "Note: WebKit2 is now built by default, you don't need to pass -2. Disable using --no-webkit2\n";
+    }
+
+    @options = (@ARGV, @options);
+
+    foreach (@features) {
+        if ($_->{define} && ${$_->{value}} != $_->{default}) {
+            my $define = lc($_->{define});
+            $define =~ s/^enable_//;
+            push @options, "WEBKIT_CONFIG" . (${$_->{value}} == 1 ? "+" : "-") . "=" . $define;
+        }
+    }
+}
+
+# If asked to build just the WebKit project, overwrite the projects
+# list after all of the port specific tweaks have been made to
+# build options, etc.
+@projects = ("Source/WebKit") if $onlyWebKitProject;
+
+if (isInspectorFrontend()) {
+    exit exitStatus(copyInspectorFrontendFiles());
+}
+
+my $result = 0;
+
+if (isWx()) {
+    $makeArgs .= " --port=wx";
+
+    downloadWafIfNeeded();
+    @options = split(/ /, $makeArgs);
+    @projects = ();
+    $result = buildWafProject('.', $clean, @options);
+    exit exitStatus($result) if exitStatus($result);
+}
+
+if (isChromium()) {
+    # Currently chromium does not honour the features passed to build-webkit.
+    # Until this is solved, we issue a warning about that.
+    foreach (@features) {
+        if (${$_->{value}} ne $_->{default}) {
+            print "\n";
+            print "===========================================================\n";
+            print " Chromium does not honor the features passed to build-webkit.\n";
+            print " The preferred way is to set up your overrides in ~/.gyp/include.gypi.\n";
+            print " See https://trac.webkit.org/wiki/Chromium#Buildingwithfeaturedefines\n";
+            print " on how to do that.\n";
+            print "===========================================================\n";
+            last;
+        }
+    }
+
+    @options = @ARGV;
+    # Chromium doesn't build by project directories.
+    @projects = ();
+    push @options, "--makeargs=" . $makeArgs if $makeArgs;
+    $result = buildChromium($clean, @options);
+    exit exitStatus($result) if exitStatus($result);
+}
+
+if (isEfl()) {
+    # By default we build using all of the available CPUs.
+    $makeArgs .= ($makeArgs ? " " : "") . "-j" . numberOfCPUs() if $makeArgs !~ /-j\s*\d+/;
+    $cmakeArgs .= ($cmakeArgs ? " " : "") . "-DENABLE_WEBKIT=ON";
+    $cmakeArgs .= " -DENABLE_WEBKIT2=ON" if !$noWebKit2;
+
+    # We remove CMakeCache to avoid the bots to reuse cached flags when
+    # we enable new features. This forces a reconfiguration.
+    removeCMakeCache();
+
+    buildCMakeProjectOrExit($clean, "Efl", $prefixPath, $makeArgs, (cmakeBasedPortArguments(), cMakeArgsFromFeatures()), $cmakeArgs);
+}
+
+if (isWinCE()) {
+    buildCMakeProjectOrExit($clean, "WinCE", $prefixPath, $makeArgs, (cmakeBasedPortArguments(), cMakeArgsFromFeatures()), $cmakeArgs);
+}
+
+if (isBlackBerry()) {
+    my $numberOfJobs;
+    if ($ENV{"USE_ICECC"}) {
+        $numberOfJobs = 50; # 50 is the number we choose for internal development
+    } else {
+        $numberOfJobs = numberOfCPUs();
+    }
+    $makeArgs .= ($makeArgs ? " " : "") . "-j" . $numberOfJobs if $makeArgs !~ /-j\s*\d+/;
+    $prefixPath = $ENV{"STAGE_DIR"} unless $prefixPath;
+    buildCMakeProjectOrExit($clean, "BlackBerry", $prefixPath, $makeArgs, (cmakeBasedPortArguments(), cMakeArgsFromFeatures()), $cmakeArgs);
+}
+
+if (isQt()) {
+    @projects = (); # An empty projects list will build the default projects
+    $result = buildQMakeProjects(\@projects, $clean, @options);
+    exit exitStatus($result) if exitStatus($result);
+}
+
+# Build, and abort if the build fails.
+for my $dir (@projects) {
+    chdir $dir or die;
+    $result = 0;
+
+    # For Gtk the WebKit project builds all others
+    if (isGtk() && $dir ne "Source/WebKit") {
+        chdirWebKit();
+        next;
+    }
+
+    my $project = basename($dir);
+    if (isGtk()) {
+        if ($noWebKit2) {
+            unshift(@options, "--disable-webkit2");
+        }
+        $result = buildGtkProject($project, $clean, @options);
+    } elsif (isAppleMacWebKit()) {
+        my @local_options = @options;
+        push @local_options, XcodeCoverageSupportOptions() if $coverageSupport && $project ne "ANGLE";
+        my $useGYPProject = $useGYP && ($project =~ "WebCore|JavaScriptCore");
+        my $projectPath = $useGYPProject ? "gyp/$project" : $project;
+        $projectPath = $project =~ /gtest/ ? "xcode/gtest" : $project;
+        $result = buildXCodeProject($projectPath, $clean, @local_options, @ARGV);
+    } elsif (isAppleWinWebKit()) {
+        if ($project eq "WebKit") {
+            $result = buildVisualStudioProject("win/WebKit.vcproj/WebKit.sln", $clean);
+        }
+    }
+    # Various build* calls above may change the CWD.
+    chdirWebKit();
+
+    if (exitStatus($result)) {
+        my $scriptDir = relativeScriptsDir();
+        if (usingVisualStudioExpress()) {
+            # Visual Studio Express is so lame it can't stdout build failures.
+            # So we find its logs and dump them to the console ourselves.
+            system(File::Spec->catfile($scriptDir, "print-vse-failure-logs"));
+        }
+        if (isAppleWinWebKit()) {
+            print "\n\n===== BUILD FAILED ======\n\n";
+            print "Please ensure you have run $scriptDir/update-webkit to install dependencies.\n\n";
+            my $baseProductDir = baseProductDir();
+            print "You can view build errors by checking the BuildLog.htm files located at:\n$baseProductDir/obj/<project>/<config>.\n";
+        }
+        exit exitStatus($result);
+    }
+}
+
+# Don't report the "WebKit is now built" message after a clean operation.
+exit if $clean;
+
+# Don't report congrats message if build was interrupted by the user.
+exit if ($result & 127) == SIGINT;
+
+# Explicitly chdir back to where exit will take us anyway, since the following "launcher"
+# message is relative to that directory.
+chdir $originalWorkingDirectory;
+
+# Write out congratulations message.
+writeCongrats();
+
+exit 0;
+
+sub cMakeArgsFromFeatures()
+{
+    my @args;
+    foreach (@features) {
+        my $featureName = $_->{define};
+        if ($featureName) {
+            my $featureEnabled = ${$_->{value}} ? "ON" : "OFF";
+            push @args, "-D$featureName=$featureEnabled";
+        }
+    }
+    return @args;
+}
+
+sub formatBuildTime($)
+{
+    my ($buildTime) = @_;
+
+    my $buildHours = int($buildTime / 3600);
+    my $buildMins = int(($buildTime - $buildHours * 3600) / 60);
+    my $buildSecs = $buildTime - $buildHours * 3600 - $buildMins * 60;
+
+    if ($buildHours) {
+        return sprintf("%dh:%02dm:%02ds", $buildHours, $buildMins, $buildSecs);
+    }
+    return sprintf("%02dm:%02ds", $buildMins, $buildSecs);
+}
+
+sub writeCongrats()
+{
+    my $launcherPath = launcherPath();
+    my $launcherName = launcherName();
+    my $endTime = time();
+    my $buildTime = formatBuildTime($endTime - $startTime);
+
+    print "\n";
+    print "===========================================================\n";
+    print " WebKit is now built ($buildTime). \n";
+    if (!isChromium()) {
+        print " To run $launcherName with this newly-built code, use the\n";
+        print " \"$launcherPath\" script.\n";
+    }
+    print "===========================================================\n";
+}
diff --git a/Tools/Scripts/build-webkittestrunner b/Tools/Scripts/build-webkittestrunner
new file mode 100755
index 0000000..e5b2354
--- /dev/null
+++ b/Tools/Scripts/build-webkittestrunner
@@ -0,0 +1,73 @@
+#!/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 "Tools/WebKitTestRunner" or die;
+
+my $result;
+if (isAppleMacWebKit()) {
+    $result = buildXCodeProject("WebKitTestRunner", $clean, XcodeOptions(), @ARGV);
+} elsif (isAppleWinWebKit()) {
+    $result = buildVisualStudioProject("WebKitTestRunner.sln", $clean);
+} elsif (isQt() || isGtk() || isEfl()) {
+    # Qt and GTK+ build everything in one shot. No need to build anything here.
+    $result = 0;
+} else {
+    die "WebKitTestRunner is not supported on this platform.\n";
+}
+
+exit exitStatus($result);
diff --git a/Tools/Scripts/check-Xcode-source-file-types b/Tools/Scripts/check-Xcode-source-file-types
new file mode 100755
index 0000000..57a70b9
--- /dev/null
+++ b/Tools/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/Tools/Scripts/check-dom-results b/Tools/Scripts/check-dom-results
new file mode 100755
index 0000000..0b32406
--- /dev/null
+++ b/Tools/Scripts/check-dom-results
@@ -0,0 +1,141 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005 Apple Computer, Inc.  All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer. 
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution. 
+# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+#     its contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission. 
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Script to check status of W3C DOM tests that are part of the WebKit tests.
+
+use strict;
+use FindBin;
+use Cwd;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+chdirWebKit();
+
+my $verbose = $ARGV[0] && $ARGV[0] eq "-v";
+
+my $workingDir = getcwd();
+my $testDirectory = "$workingDir/LayoutTests";
+
+my @suites = ( {"name" => "DOM Level 1 Core   (html)", "directory" => "dom/html/level1/core"},
+               {"name" => "DOM Level 2 Core   (html)", "directory" => "dom/html/level2/core"},
+               {"name" => "DOM Level 2 Events (html)", "directory" => "dom/html/level2/events"},
+               {"name" => "DOM Level 2 HTML   (html)", "directory" => "dom/html/level2/html"},
+               {"name" => "DOM Level 1 Core   (xhtml)", "directory" => "dom/xhtml/level1/core"},
+               {"name" => "DOM Level 2 Core   (xhtml)", "directory" => "dom/xhtml/level2/core"},
+               {"name" => "DOM Level 2 Events (xhtml)", "directory" => "dom/xhtml/level2/events"},
+               {"name" => "DOM Level 2 HTML   (xhtml)", "directory" => "dom/xhtml/level2/html"},
+               {"name" => "DOM Level 3 Core   (xhtml)", "directory" => "dom/xhtml/level3/core"},
+               {"name" => "DOM Level 3 XPath  (svg)", "directory" => "dom/svg/level3/xpath"});
+
+my $totalCount = 0;
+my $totalSuccesses = 0;
+my $totalDisabled = 0;
+my $totalFailed = 0;
+
+foreach my $suite (@suites) {
+
+    my %suite = %$suite;
+    my $directory = $suite{"directory"};
+    my $name = $suite{"name"};
+    my @results = `find "${testDirectory}/${directory}" -name "*-expected.txt"`;
+    my @disabled = `find "${testDirectory}/${directory}" -name "*-disabled"`;
+
+    my @failures = ();
+    my $count = 0;
+
+    foreach my $result (@results) {
+        $count++;
+        my $success = 0;
+        open RESULT, "<$result";
+        while (<RESULT>) {
+            if (/Success/) {
+                $success = 1;
+                last;
+            }
+        }
+        close RESULT;
+        if (!$success) {
+            push @failures, $result;
+        }
+    }
+
+    my $disabledCount = (scalar @disabled);
+    my $failureCount = (scalar @failures);
+
+    $count += $disabledCount;
+
+    my $successCount = $count - $failureCount - $disabledCount;
+    my $percentage = (sprintf "%.1f", ($successCount * 100.0 / $count));
+    
+    if ($percentage == 100) {
+        print "${name}: all ${count} tests succeeded";
+    } else {
+        print "${name}: ${successCount} out of ${count} tests succeeded (${percentage}%)";
+    }
+    print " ($disabledCount disabled)" if $disabledCount;
+    print "\n";
+    if ($verbose) {
+        print "\n";
+        if (@disabled) {
+            print "    Disabled:\n";
+            
+            foreach my $failure (sort @disabled) {
+                $failure =~ s|.*/||;
+                $failure =~ s|-disabled||;
+                print "        ${directory}/${failure}";
+            }
+        }
+        if (@failures) {
+            print "    Failed:\n";
+            
+            foreach my $failure (sort @failures) {
+                $directory =~ m|^dom/(\w+)|;
+                my $extension = $1;
+                $failure =~ s|.*/||;
+                $failure =~ s|-expected\.txt|.${extension}|;
+                print "        ${directory}/${failure}";
+            }
+        }
+
+        print "\n";
+    }
+
+    $totalCount += $count;
+    $totalSuccesses += $successCount;
+    $totalDisabled += $disabledCount;
+    $totalFailed += $failureCount;
+}
+
+
+my $totalPercentage = (sprintf "%.1f", ($totalSuccesses * 100.0 / $totalCount));
+my $totalDisabledPercentage = (sprintf "%.1f", ($totalDisabled * 100.0 / $totalCount));
+my $totalFailedPercentage = (sprintf "%.1f", ($totalFailed * 100.0 / $totalCount));
+
+print "Total: ${totalSuccesses} out of ${totalCount} tests succeeded (${totalPercentage}%)\n";
+print "       ${totalDisabled} tests disabled (${totalDisabledPercentage}%)\n";
+print "       ${totalFailed} tests failed (${totalFailedPercentage}%)\n";
diff --git a/Tools/Scripts/check-for-exit-time-destructors b/Tools/Scripts/check-for-exit-time-destructors
new file mode 100755
index 0000000..ae8fc0d
--- /dev/null
+++ b/Tools/Scripts/check-for-exit-time-destructors
@@ -0,0 +1,152 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2006, 2007, 2008 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.
+
+# "check-for-exit-time-destructors" script for WebKit Open Source Project
+
+# Intended to be invoked from an Xcode build step to check if there are
+# any exit-time destructors in a target.
+
+use warnings;
+use strict;
+
+use File::Basename;
+
+sub touch($);
+sub printFunctions($$);
+
+my $arch = $ENV{'CURRENT_ARCH'};
+my $configuration = $ENV{'CONFIGURATION'};
+my $target = $ENV{'TARGET_NAME'};
+my $variant = $ENV{'CURRENT_VARIANT'};
+my $coverageBuild = $ENV{'WEBKIT_COVERAGE_BUILD'};
+my $debugRoot = $ENV{'WEBKIT_DEBUG_ROOT'};
+
+$arch = $ENV{'NATIVE_ARCH'} if !$arch; # for Xcode 2.1, which does not have CURRENT_ARCH
+$variant = "normal" if !$variant; # for Xcode 2.1, which does not have CURRENT_VARIANT
+
+my $executablePath = "$ENV{'TARGET_BUILD_DIR'}/$ENV{'EXECUTABLE_PATH'}";
+
+my $buildTimestampPath = $ENV{'TARGET_TEMP_DIR'} . "/" . basename($0) . ".timestamp";
+my $buildTimestampAge = -M $buildTimestampPath;
+my $scriptAge = -M $0;
+
+my $list = $ENV{"LINK_FILE_LIST_${variant}_${arch}"};
+
+if (!open LIST, $list) {
+    print "ERROR: Could not open $list\n";
+    exit 1;
+}
+
+my @files = <LIST>;
+chomp @files;
+close LIST;
+
+my $sawError = 0;
+
+for my $file (sort @files) {
+    if (defined $buildTimestampAge && $buildTimestampAge < $scriptAge) {
+        my $fileAge = -M $file;
+        next if defined $fileAge && $fileAge > $buildTimestampAge;
+    }
+    if (!open NM, "(nm '$file' | sed 's/^/STDOUT:/') 2>&1 |") {
+        print "ERROR: Could not open $file\n";
+        $sawError = 1;
+        next;
+    }
+    my $sawAtExit = 0;
+    my $shortName = $file;
+    $shortName =~ s/.*\///;
+
+    while (<NM>) {
+        if (/^STDOUT:/) {
+            # With GC logging enabled Heap.o may contain finalizers, so we ignore them.
+            $sawAtExit = 1 if (/___cxa_atexit/ && ($shortName ne "Heap.o"));
+        } else {
+            print STDERR if $_ ne "nm: no name list\n";
+        }
+    }
+    close NM;
+    next unless $sawAtExit;
+
+    $sawError = 1 if printFunctions($shortName, $file);
+}
+
+if ($sawError and !$coverageBuild) {
+    print "ERROR: Use DEFINE_STATIC_LOCAL from <wtf/StdLibExtras.h>\n";
+    unlink $executablePath;
+    exit 1;
+}
+
+touch($buildTimestampPath);
+exit 0;
+
+sub touch($)
+{
+    my ($path) = @_;
+    open(TOUCH, ">", $path) or die "$!";
+    close(TOUCH);
+}
+
+sub demangle($)
+{
+    my ($symbol) = @_;
+    if (!open FILT, "c++filt $symbol |") {
+        print "ERROR: Could not open c++filt\n";
+        return;
+    }
+    my $result = <FILT>;
+    close FILT;
+    chomp $result;
+    return $result;
+}
+
+sub printFunctions($$)
+{
+    my ($shortName, $path) = @_;
+    if (!open OTOOL, "otool -tV '$path' |") {
+        print "WARNING: Could not open $path\n";
+        return 0;
+    }
+    my %functions;
+    my $currentSymbol = "";
+    while (<OTOOL>) {
+        $currentSymbol = $1 if /^(\w+):$/;
+        next unless $currentSymbol;
+        $functions{demangle($currentSymbol)} = 1 if /___cxa_atexit/;
+    }
+    close OTOOL;
+    my $result = 0;
+    for my $function (sort keys %functions) {
+        if (!$result) {
+            print "ERROR: $shortName has exit time destructors in it! ($path)\n";
+            $result = 1;
+        }
+        print "ERROR: In function $function\n";
+    }
+    return $result;
+}
diff --git a/Tools/Scripts/check-for-global-initializers b/Tools/Scripts/check-for-global-initializers
new file mode 100755
index 0000000..7f90577
--- /dev/null
+++ b/Tools/Scripts/check-for-global-initializers
@@ -0,0 +1,167 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2006, 2007, 2008 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.
+
+# "check-for-global-initializers" script for WebKit Open Source Project
+
+# Intended to be invoked from an Xcode build step to check if there are
+# any global initializers in a target.
+
+use warnings;
+use strict;
+
+use File::Basename;
+
+sub touch($);
+sub demangle($);
+
+my $arch = $ENV{'CURRENT_ARCH'};
+my $configuration = $ENV{'CONFIGURATION'};
+my $target = $ENV{'TARGET_NAME'};
+my $variant = $ENV{'CURRENT_VARIANT'};
+my $coverageBuild = $ENV{'WEBKIT_COVERAGE_BUILD'};
+my $debugRoot = $ENV{'WEBKIT_DEBUG_ROOT'};
+
+$arch = $ENV{'NATIVE_ARCH'} if !$arch; # for Xcode 2.1, which does not have CURRENT_ARCH
+$variant = "normal" if !$variant; # for Xcode 2.1, which does not have CURRENT_VARIANT
+
+my $executablePath = "$ENV{'TARGET_BUILD_DIR'}/$ENV{'EXECUTABLE_PATH'}";
+
+my $buildTimestampPath = $ENV{'TARGET_TEMP_DIR'} . "/" . basename($0) . ".timestamp";
+my $buildTimestampAge = -M $buildTimestampPath;
+my $scriptAge = -M $0;
+
+my $list = $ENV{"LINK_FILE_LIST_${variant}_${arch}"};
+
+if (!open LIST, $list) {
+    print "ERROR: Could not open $list\n";
+    exit 1;
+}
+
+my @files = <LIST>;
+chomp @files;
+close LIST;
+
+my $sawError = 0;
+
+for my $file (sort @files) {
+    if (defined $buildTimestampAge && $buildTimestampAge < $scriptAge) {
+        my $fileAge = -M $file;
+        next if defined $fileAge && $fileAge > $buildTimestampAge;
+    }
+    if (!open NM, "(nm '$file' | sed 's/^/STDOUT:/') 2>&1 |") {
+        print "ERROR: Could not open $file\n";
+        $sawError = 1;
+        next;
+    }
+    my $sawGlobal = 0;
+    my @globals;
+    while (<NM>) {
+        if (/^STDOUT:/) {
+            my $line = $_;
+            if ($line =~ /__GLOBAL__I(.+)$/) {
+                $sawGlobal = 1;
+                push(@globals, demangle($1));
+            }
+        } else {
+            print STDERR if $_ ne "nm: no name list\n";
+        }
+    }
+    close NM;
+    if ($sawGlobal) {
+        my $shortName = $file;
+        $shortName =~ s/.*\///;
+
+        # Special cases for files that have initializers in debug builds.
+        if ($configuration eq "Debug" or $variant eq "debug" or $debugRoot) {
+            if ($target eq "JavaScriptCore") {
+                next if $shortName eq "AllInOneFile.o";
+                next if $shortName eq "Opcode.o";
+                next if $shortName eq "Structure.o";
+                next if $shortName eq "nodes.o";
+            }
+            if ($target eq "WebCore") {
+                next if $shortName eq "BidiRun.o";
+                next if $shortName eq "CachedPage.o";
+                next if $shortName eq "CachedResource.o";
+                next if $shortName eq "FEGaussianBlur.o";
+                next if $shortName eq "Frame.o";
+                next if $shortName eq "JSCustomSQLTransactionCallback.o";
+                next if $shortName eq "JSLazyEventListener.o";
+                next if $shortName eq "Node.o";
+                next if $shortName eq "Page.o";
+                next if $shortName eq "Range.o";
+                next if $shortName eq "RenderObject.o";
+                next if $shortName eq "SVGElementInstance.o";
+                next if $shortName eq "SubresourceLoader.o";
+                next if $shortName eq "XMLHttpRequest.o";
+            }
+            if ($target eq "WebKit") {
+                next if $shortName eq "HostedNetscapePluginStream.o";
+                next if $shortName eq "NetscapePluginInstanceProxy.o";
+            }
+            if ($target eq "WebKit2") {
+                next if $shortName eq "WebContext.o";
+                next if $shortName eq "WebFrame.o";
+                next if $shortName eq "WebPage.o";
+                next if $shortName eq "WebPageProxy.o";
+            }
+        }
+
+        print "ERROR: $shortName has one or more global initializers in it! ($file), near @globals\n";
+        $sawError = 1;
+    }
+}
+
+if ($sawError and !$coverageBuild) {
+    unlink $executablePath;
+    exit 1;
+}
+
+touch($buildTimestampPath);
+exit 0;
+
+sub touch($)
+{
+    my ($path) = @_;
+    open(TOUCH, ">", $path) or die "$!";
+    close(TOUCH);
+}
+
+sub demangle($)
+{
+    my ($symbol) = @_;
+    if (!open FILT, "c++filt $symbol |") {
+        print "ERROR: Could not open c++filt\n";
+        return;
+    }
+    my $result = <FILT>;
+    close FILT;
+    chomp $result;
+    return $result;
+}
+
diff --git a/Tools/Scripts/check-for-inappropriate-files-in-framework b/Tools/Scripts/check-for-inappropriate-files-in-framework
new file mode 100755
index 0000000..41e7c84
--- /dev/null
+++ b/Tools/Scripts/check-for-inappropriate-files-in-framework
@@ -0,0 +1,70 @@
+#!/usr/bin/env ruby
+
+# 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.
+
+base_directory = ENV['TARGET_BUILD_DIR'] or throw "Unable to find TARGET_BUILD_DIR in the environment!"
+project_name = ENV['PROJECT_NAME'] or throw "Unable to find PROJECT_NAME in the environment!"
+is_shallow_bundle = (ENV['SHALLOW_BUNDLE'] || "NO").upcase == "YES"
+
+$INAPPROPRIATE_FILES = { "WebCore" => { "Resources" => ["*.css", "*.in", "*.idl", "*.h"] },
+                         "WebKit2" => { "Resources" => ["*.in", "*.h"] },
+                       }
+
+Dir.chdir base_directory
+
+$error_printed = false
+
+def print_error msg
+  $error_printed = true
+  STDERR.puts "ERROR: #{msg}"
+end
+
+def print_inappropriate_file_error framework, relative_path
+  print_error "#{framework}.framework/#{relative_path} should not be present in the framework."
+end
+
+def check_framework framework, is_shallow_bundle
+  $INAPPROPRIATE_FILES[framework].each do |directory, patterns|
+    framework_bundle_path = is_shallow_bundle ? "#{framework}.framework" : "#{framework}.framework/Versions/A/#{directory}"
+    Dir.chdir framework_bundle_path do
+      patterns.each do |pattern|
+        Dir.glob(pattern).each do |inappropriate_file|
+          print_inappropriate_file_error framework, is_shallow_bundle ? inappropriate_file : "#{directory}/#{inappropriate_file}"
+          File.unlink inappropriate_file
+        end
+      end
+    end
+  end
+end
+
+check_framework project_name, is_shallow_bundle
+
+if $error_printed
+  STDERR.puts
+  STDERR.puts "    Inappropriate files were detected and have been removed from the framework."
+  STDERR.puts "    If this error continues to appear after building again then the build system needs"
+  STDERR.puts "    to be modified so that the inappropriate files are no longer copied in to the framework."
+  STDERR.puts
+  exit 1
+end
diff --git a/Tools/Scripts/check-for-inappropriate-objc-class-names b/Tools/Scripts/check-for-inappropriate-objc-class-names
new file mode 100755
index 0000000..5ee8410
--- /dev/null
+++ b/Tools/Scripts/check-for-inappropriate-objc-class-names
@@ -0,0 +1,114 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2006, 2007, 2008, 2010, 2011 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.
+
+# "check-for-inappropriate-objc-class-names" script for WebKit Open Source Project
+
+# Intended to be invoked from an Xcode build step to check if a framework
+# defines any Objective-C class whose name does not have one of the prefixes
+# the framework is allowed to use.
+
+use warnings;
+use strict;
+
+use File::Basename;
+
+sub touch($);
+
+my @allowedPrefixes = @ARGV;
+
+# Xcode will automatically link ObjC binaries against libarclite in some cases, which defines a class called __ARCLite__.
+push(@allowedPrefixes, "__ARCLite");
+
+die "No allowed prefixes passed on the command line" if !@allowedPrefixes;
+
+my $arch = $ENV{'CURRENT_ARCH'};
+my $target = $ENV{'TARGET_NAME'};
+my $variant = $ENV{'CURRENT_VARIANT'};
+my $coverageBuild = $ENV{'WEBKIT_COVERAGE_BUILD'};
+
+my $executablePath = "$ENV{'TARGET_BUILD_DIR'}/$ENV{'EXECUTABLE_PATH'}";
+
+my $buildTimestampPath = $ENV{'TARGET_TEMP_DIR'} . "/" . basename($0) . join('-', @allowedPrefixes) . ".timestamp";
+my $buildTimestampAge = -M $buildTimestampPath;
+my $executablePathAge = -M $executablePath;
+my $scriptAge = -M $0;
+
+my $pattern = "^(" . join('|', @allowedPrefixes) . ")";
+
+my $sawError = 0;
+
+if (!defined $executablePathAge || !defined $buildTimestampAge || $executablePathAge < $buildTimestampAge || $scriptAge < $buildTimestampAge) {
+    if (!open NM, "(nm -Ugjp '$executablePath' | sed 's/^/STDOUT:/') 2>&1 |") {
+        print "ERROR: Could not open $executablePath\n";
+        $sawError = 1;
+        next;
+    }
+    my @badNames;
+    while (<NM>) {
+        if (/^STDOUT:/) {
+            next unless /^STDOUT:_OBJC_CLASS_\$_/;
+            chomp;
+            my $className = substr($_, 21);
+            push(@badNames, $className) unless $className =~ /$pattern/;
+        } else {
+            print STDERR if $_ ne "nm: no name list\n";
+        }
+    }
+    close NM;
+
+    if (@badNames) {
+
+        my $shortName = $executablePath;
+        $shortName =~ s/.*\///;
+
+        print "ERROR: $shortName defines one or more Objective-C classes with inappropriate names. ($executablePath)\n";
+        for my $className (@badNames) {
+            print "ERROR: Inappropriate Objective-C class name: $className.\n";
+        }
+
+        if (@allowedPrefixes > 1) {
+            print "ERROR: Objective-C class names in $target must have one of these prefixes: " . join(", ", map('"' . $_ . '"', @allowedPrefixes)) . ".\n";
+        } else {
+            print "ERROR: Objective-C class names in $target must have the prefix \"" . $allowedPrefixes[0] . "\".\n";
+        }
+
+        $sawError = 1;
+    }
+}
+
+if ($sawError and !$coverageBuild) {
+    unlink $executablePath;
+    exit 1;
+}
+
+touch($buildTimestampPath);
+exit 0;
+
+sub touch($)
+{
+    my ($path) = @_;
+    open(TOUCH, ">", $path) or die "$!";
+    close(TOUCH);
+}
diff --git a/Tools/Scripts/check-for-weak-vtables-and-externals b/Tools/Scripts/check-for-weak-vtables-and-externals
new file mode 100755
index 0000000..dfbf89f
--- /dev/null
+++ b/Tools/Scripts/check-for-weak-vtables-and-externals
@@ -0,0 +1,120 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2006, 2007, 2008, 2010 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# 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 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.
+
+# "check-for-weak-vtables-and-externals" script for WebKit Open Source Project
+
+# Intended to be invoked from an Xcode build step to check if there are
+# any weak vtables or weak externals in a target.
+
+use warnings;
+use strict;
+
+use File::Basename;
+
+sub touch($);
+
+my $arch = $ENV{'CURRENT_ARCH'};
+my $configuration = $ENV{'CONFIGURATION'};
+my $target = $ENV{'TARGET_NAME'};
+my $variant = $ENV{'CURRENT_VARIANT'};
+my $coverageBuild = $ENV{'WEBKIT_COVERAGE_BUILD'};
+my $debugRoot = $ENV{'WEBKIT_DEBUG_ROOT'};
+
+$arch = $ENV{'NATIVE_ARCH'} if !$arch; # for Xcode 2.1, which does not have CURRENT_ARCH
+$variant = "normal" if !$variant; # for Xcode 2.1, which does not have CURRENT_VARIANT
+
+my $executablePath = "$ENV{'TARGET_BUILD_DIR'}/$ENV{'EXECUTABLE_PATH'}";
+
+my $buildTimestampPath = $ENV{'TARGET_TEMP_DIR'} . "/" . basename($0) . ".timestamp";
+my $buildTimestampAge = -M $buildTimestampPath;
+my $executablePathAge = -M $executablePath;
+
+my $sawError = 0;
+
+if (!defined $executablePathAge || !defined $buildTimestampAge || $executablePathAge < $buildTimestampAge) {
+    if (!open NM, "(nm -m '$executablePath' | c++filt | sed 's/^/STDOUT:/') 2>&1 |") {
+        print "ERROR: Could not open $executablePath\n";
+        $sawError = 1;
+        next;
+    }
+    my @weakVTableClasses = ();
+    my @weakExternalSymbols = ();
+    while (<NM>) {
+        if (/^STDOUT:/) {
+            # Ignore undefined, RTTI and typeinfo symbols.
+            next if /\bundefined\b/ or /\b__ZT[IS]/;
+
+            if (/weak external vtable for (.*)$/) {
+                push @weakVTableClasses, $1;
+            } elsif (/weak external (.*)$/) {
+                push @weakExternalSymbols, $1;
+            }
+        } else {
+            print STDERR if $_ ne "nm: no name list\n";
+        }
+    }
+    close NM;
+
+    my $shortName = $executablePath;
+    $shortName =~ s/.*\///;
+
+    if (@weakVTableClasses) {
+        print "ERROR: $shortName has a weak vtable in it ($executablePath)\n";
+        print "ERROR: Fix by making sure the first virtual function in each of these classes is not an inline:\n";
+        for my $class (sort @weakVTableClasses) {
+            print "ERROR: class $class\n";
+        }
+        $sawError = 1;
+    }
+
+    if (@weakExternalSymbols) {
+        print "ERROR: $shortName has a weak external symbol in it ($executablePath)\n";
+        print "ERROR: A weak external symbol is generated when a symbol is defined in multiple compilation units and is also marked as being exported from the library.\n";
+        print "ERROR: A common cause of weak external symbols is when an inline function is listed in the linker export file.\n";
+        for my $symbol (sort @weakExternalSymbols) {
+            print "ERROR: symbol $symbol\n";
+        }
+        $sawError = 1;
+    }
+}
+
+if ($sawError and !$coverageBuild) {
+    unlink $executablePath;
+    exit 1;
+}
+
+touch($buildTimestampPath);
+
+exit 0;
+
+sub touch($)
+{
+    my ($path) = @_;
+    open(TOUCH, ">", $path) or die "$!";
+    close(TOUCH);
+}
diff --git a/Tools/Scripts/check-for-webkit-framework-include-consistency b/Tools/Scripts/check-for-webkit-framework-include-consistency
new file mode 100755
index 0000000..339fa7e
--- /dev/null
+++ b/Tools/Scripts/check-for-webkit-framework-include-consistency
@@ -0,0 +1,111 @@
+#!/usr/bin/env ruby
+
+# 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.
+
+
+base_directory = ENV['TARGET_BUILD_DIR'] or throw "Unable to find TARGET_BUILD_DIR in the environment!"
+is_shallow_bundle = (ENV['SHALLOW_BUNDLE'] || "NO").upcase == "YES"
+
+unless base_directory
+  throw "Unable to find TARGET_BUILD_DIR in the environment!"
+end
+
+Dir.chdir base_directory
+
+$PERMITTED_INCLUDE_TYPES = { :public => [ :public ], :private => [ :public, :private ] }
+
+$HEADER_NAMES_TO_TYPE = { }
+$HEADERS_BY_TYPE = { :public => [], :private => [] }
+
+$error_printed = false
+
+def print_error msg
+  $error_printed = true
+  STDERR.puts "ERROR: #{msg}"
+end
+
+def build_header_maps is_shallow_bundle
+  current_version_path = is_shallow_bundle ? "" : "Versions/A/"
+  all_headers = `find WebKit.framework/#{current_version_path}{,Private}Headers -type f -name '*.h'`.split
+
+  all_headers.each do |header|
+    if /\/Headers\/(.*)/.match(header)
+      $HEADER_NAMES_TO_TYPE[$1] = :public
+      $HEADERS_BY_TYPE[:public] << header
+    elsif /\/PrivateHeaders\/(.*)/.match(header)
+      $HEADER_NAMES_TO_TYPE[$1] = :private
+      $HEADERS_BY_TYPE[:private] << header
+    else
+      print_error "Unknown header type: #{header}"
+    end
+  end
+end
+
+def resolve_include(header, included_header, permitted_types)
+  # Ignore includes that aren't in the typical framework style.
+  return unless /<([^\/]+)\/(.*)>/.match(included_header)
+
+  framework, included_header_name = [$1, $2]
+
+  # Ignore includes that aren't related to other WebKit headers.
+  return unless framework =~ /^Web/
+
+  # A header of any type including a WebCore header is a recipe for disaster.
+  if framework == "WebCore"
+    # <rdar://problem/7718826> WebKeyGenerator.h should not include a WebCore header
+    return if header =~ /\/WebKeyGenerator.h$/ and included_header_name == "WebCoreKeyGenerator.h"
+
+    print_error "#{header} included #{included_header}!"
+    return
+  end
+
+  header_type = $HEADER_NAMES_TO_TYPE[included_header_name]
+
+  if not header_type
+    print_error "#{header} included #{included_header} but I could not find a header of that name!"
+  elsif not permitted_types.member?(header_type)
+    print_error "#{header} included #{included_header} which is #{header_type}!"
+  end
+end
+
+def verify_includes(header, permitted_types)
+  File.open(header) do |file|
+    file.each_line do |line|
+      if /#(include|import) (.*)/.match(line)
+        resolve_include(header, $2, permitted_types)
+      end
+    end
+  end
+end
+
+build_header_maps is_shallow_bundle
+
+$HEADERS_BY_TYPE.each do |header_type, headers|
+  permitted_types = $PERMITTED_INCLUDE_TYPES[header_type]
+  headers.each do |header|
+    verify_includes header, permitted_types
+  end
+end
+
+exit 1 if $error_printed
diff --git a/Tools/Scripts/check-inspector-strings b/Tools/Scripts/check-inspector-strings
new file mode 100755
index 0000000..267c03a
--- /dev/null
+++ b/Tools/Scripts/check-inspector-strings
@@ -0,0 +1,171 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2011 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 codecs
+import logging
+import os
+import os.path
+import re
+import sys
+
+from webkitpy.common.checkout.scm import SCMDetector
+from webkitpy.common.system.filesystem import FileSystem
+from webkitpy.common.system.executive import Executive
+from webkitpy.common.system.logutils import configure_logging
+from webkitpy.style.checker import ProcessorBase
+from webkitpy.style.filereader import TextFileReader
+from webkitpy.style.main import change_directory
+
+_inspector_directory = "Source/WebCore/inspector/front-end"
+_devtools_directory = "Source/WebKit/chromium/src/js"
+_localized_strings = "Source/WebCore/English.lproj/localizedStrings.js"
+
+_log = logging.getLogger("check-inspector-strings")
+
+class StringsExtractor(ProcessorBase):
+    def __init__(self, patterns):
+        self._patterns = patterns
+        self.strings = []
+        for p in self._patterns:
+            self.strings.append([])
+
+    def should_process(self, file_path):
+        return file_path.endswith(".js") and (not file_path.endswith("InjectedScript.js"))
+
+    def process(self, lines, file_path, line_numbers=None):
+        for line in lines:
+            comment_start = line.find("//")
+            if comment_start != -1:
+                line = line[:comment_start]
+            index = 0
+            for pattern in self._patterns:
+                line_strings = re.findall(pattern, line)
+                for string in line_strings:
+                    self.strings[index].append(string)
+                index += 1
+
+class LocalizedStringsExtractor:
+    def __init__(self):
+        self.localized_strings = []
+
+    def process_file(self, file_path):
+        localized_strings_file = codecs.open(file_path, encoding="utf-8", mode="r")
+        try:
+            contents = localized_strings_file.read()
+            lines = contents.split("\n")
+            for line in lines:
+                match = re.match(r"localizedStrings\[\"((?:[^\"\\]|\\.)*?)\"", line)
+                if match:
+                    self.localized_strings.append(match.group(1))
+        finally:
+            localized_strings_file.close()
+
+def extract_ui_strings(str, out):
+    line_unrecognized = False
+    idx = 0
+    while idx < len(str):
+        idx = str.find("WebInspector.UIString(", idx)
+        if idx == -1:
+            break
+        idx = idx + len("WebInspector.UIString(")
+        balance = 1
+        item_recognized = False
+        while idx < len(str):
+            if str[idx] == ')':
+                balance = balance - 1
+                if balance == 0:
+                    break
+            elif str[idx] == '(':
+                balance = balance + 1
+            elif balance == 1:
+                if str[idx] == ',':
+                    break
+                elif str[idx] == '"':
+                    str_idx = idx + 1
+                    while str_idx < len(str):
+                        if str[str_idx] == '\\':
+                            str_idx = str_idx + 1
+                        elif str[str_idx] == '"':
+                            out.add(str[idx + 1 : str_idx])
+                            idx = str_idx
+                            item_recognized = True
+                            break
+                        str_idx = str_idx + 1
+            idx = idx + 1
+        if not item_recognized:
+            line_unrecognized = True
+    if line_unrecognized:
+        _log.info("Unrecognized: %s" % str)
+
+if __name__ == "__main__":
+    configure_logging()
+
+    cwd = os.path.abspath(os.curdir)
+    filesystem = FileSystem()
+    scm = SCMDetector(filesystem, Executive()).detect_scm_system(cwd)
+
+    if scm is None:
+        _log.error("WebKit checkout not found: You must run this script "
+                   "from within a WebKit checkout.")
+        sys.exit(1)
+
+    checkout_root = scm.checkout_root
+    _log.debug("WebKit checkout found with root: %s" % checkout_root)
+    change_directory(filesystem, checkout_root=checkout_root, paths=None)
+
+    strings_extractor = StringsExtractor([r"(WebInspector\.UIString\(.*)", r"\"((?:[^\"\\]|\\.)*?)\""])
+    file_reader = TextFileReader(filesystem, strings_extractor)
+    file_reader.process_paths([_inspector_directory, _devtools_directory])
+    localized_strings_extractor = LocalizedStringsExtractor()
+    localized_strings_extractor.process_file(_localized_strings)
+    raw_ui_strings = frozenset(strings_extractor.strings[0])
+    ui_strings = set()
+    for s in raw_ui_strings:
+        extract_ui_strings(s, ui_strings)
+    strings = frozenset(strings_extractor.strings[1])
+    localized_strings = frozenset(localized_strings_extractor.localized_strings)
+
+    new_strings = ui_strings - localized_strings
+    for s in new_strings:
+        _log.info("New: \"%s\"" % (s))
+    old_strings = localized_strings - ui_strings
+    suspicious_strings = strings & old_strings
+    for s in suspicious_strings:
+        _log.info("Suspicious: \"%s\"" % (s))
+    unused_strings = old_strings - strings
+    for s in unused_strings:
+        _log.info("Unused: \"%s\"" % (s))
+
+    localized_strings_duplicates = {}
+    for s in localized_strings_extractor.localized_strings:
+        if s in localized_strings_duplicates:
+            _log.info("Duplicate: \"%s\"" % (s))
+        else:
+            localized_strings_duplicates.setdefault(s)
diff --git a/Tools/Scripts/check-webkit-style b/Tools/Scripts/check-webkit-style
new file mode 100755
index 0000000..54ca276
--- /dev/null
+++ b/Tools/Scripts/check-webkit-style
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2011 Google Inc. All rights reserved.
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+#
+# 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.
+
+"""Does WebKit-lint on C/C++ or text files.
+
+The goal of this script is to identify places in the code that *may*
+be in non-compliance with WebKit style.  It does not attempt to fix
+up these problems -- the point is to educate.  It does also not
+attempt to find all problems, or to ensure that everything it does
+find is legitimately a problem."""
+
+import sys
+
+import webkitpy.common.version_check
+
+from webkitpy.style.main import CheckWebKitStyle
+
+
+if __name__ == "__main__":
+    sys.exit(CheckWebKitStyle().main())
diff --git a/Tools/Scripts/clean-header-guards b/Tools/Scripts/clean-header-guards
new file mode 100755
index 0000000..2bad046
--- /dev/null
+++ b/Tools/Scripts/clean-header-guards
@@ -0,0 +1,53 @@
+#!/usr/bin/ruby
+
+require 'find'
+require 'optparse'
+
+options = {}
+OptionParser.new do |opts|
+  opts.banner = "Usage: clean-header-guards [options]"
+  
+  opts.on("--prefix [PREFIX]", "Append a header prefix to all guards") do |prefix|
+    options[:prefix] = prefix
+  end
+end.parse!
+
+IgnoredFilenamePatterns = [
+  # ignore headers which are known not to have guard
+  /WebCorePrefix/, 
+  /ForwardingHeaders/,
+  %r|bindings/objc|, 
+  /vcproj/, # anything inside a vcproj is in the windows wasteland
+  
+  # we don't own any of these headers
+  %r|icu/unicode|,
+  %r|platform/graphics/cairo|,
+  %r|platform/image-decoders|,
+  
+  /config.h/ # changing this one sounds scary
+].freeze
+
+IgnoreFileNamesPattern = Regexp.union(*IgnoredFilenamePatterns).freeze
+
+Find::find(".") do |filename|
+  next unless filename =~ /\.h$/
+  next if filename.match(IgnoreFileNamesPattern)
+  
+  File.open(filename, "r+") do |file|
+    contents = file.read
+    match_results = contents.match(/#ifndef (\S+)\n#define \1/s)
+    if match_results
+      current_guard = match_results[1]
+      new_guard = File.basename(filename).sub('.', '_')
+      new_guard = options[:prefix] + '_' + new_guard  if options[:prefix]
+      contents.gsub!(/#{current_guard}\b/, new_guard)
+    else
+      puts "Ignoring #{filename}, failed to find existing header guards."
+    end
+    tmp_filename = filename + ".tmp"
+    File.open(tmp_filename, "w+") do |new_file|
+      new_file.write(contents)
+    end
+    File.rename tmp_filename, filename
+  end
+end
diff --git a/Tools/Scripts/commit-log-editor b/Tools/Scripts/commit-log-editor
new file mode 100755
index 0000000..e790569
--- /dev/null
+++ b/Tools/Scripts/commit-log-editor
@@ -0,0 +1,371 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2006, 2007, 2008, 2009, 2010 Apple Inc.  All rights reserved.
+# Copyright (C) 2009 Torch Mobile 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.
+
+# Script to put change log comments in as default check-in comment.
+
+use strict;
+use Getopt::Long;
+use File::Basename;
+use File::Spec;
+use FindBin;
+use lib $FindBin::Bin;
+use VCSUtils;
+use webkitdirs;
+
+sub createCommitMessage(@);
+sub loadTermReadKey();
+sub normalizeLineEndings($$);
+sub patchAuthorshipString($$$);
+sub removeLongestCommonPrefixEndingInDoubleNewline(\%);
+sub isCommitLogEditor($);
+
+my $endl = "\n";
+
+sub printUsageAndExit
+{
+    my $programName = basename($0);
+    print STDERR <<EOF;
+Usage: $programName [--regenerate-log] <log file>
+       $programName --print-log <ChangeLog file> [<ChangeLog file>...]
+       $programName --help
+EOF
+    exit 1;
+}
+
+my $help = 0;
+my $printLog = 0;
+my $regenerateLog = 0;
+
+my $getOptionsResult = GetOptions(
+    'help' => \$help,
+    'print-log' => \$printLog,
+    'regenerate-log' => \$regenerateLog,
+);
+
+if (!$getOptionsResult || $help) {
+    printUsageAndExit();
+}
+
+die "Can't specify both --print-log and --regenerate-log\n" if $printLog && $regenerateLog;
+
+if ($printLog) {
+    printUsageAndExit() unless @ARGV;
+    print createCommitMessage(@ARGV);
+    exit 0;
+}
+
+my $log = $ARGV[0];
+if (!$log) {
+    printUsageAndExit();
+}
+
+my $baseDir = baseProductDir();
+
+my $editor = $ENV{SVN_LOG_EDITOR};
+$editor = $ENV{CVS_LOG_EDITOR} if !$editor;
+$editor = "" if $editor && isCommitLogEditor($editor);
+
+my $splitEditor = 1;
+if (!$editor) {
+    my $builtEditorApplication = "$baseDir/Release/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
+    if (-x $builtEditorApplication) {
+        $editor = $builtEditorApplication;
+        $splitEditor = 0;
+    }
+}
+if (!$editor) {
+    my $builtEditorApplication = "$baseDir/Debug/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
+    if (-x $builtEditorApplication) {
+        $editor = $builtEditorApplication;
+        $splitEditor = 0;
+    }
+}
+if (!$editor) {
+    my $builtEditorApplication = "$ENV{HOME}/Applications/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
+    if (-x $builtEditorApplication) {
+        $editor = $builtEditorApplication;
+        $splitEditor = 0;
+    }
+}
+
+$editor = $ENV{EDITOR} if !$editor;
+$editor = "/usr/bin/vi" if !$editor;
+
+my @editor;
+if ($splitEditor) {
+    @editor = split ' ', $editor;
+} else {
+    @editor = ($editor);
+}
+
+my $inChangesToBeCommitted = !isGit();
+my @changeLogs = ();
+my $logContents = "";
+my $existingLog = 0;
+open LOG, $log or die "Could not open the log file.";
+while (my $curLine = <LOG>) {
+    if (isGit()) {
+        if ($curLine =~ /^# Changes to be committed:$/) {
+            $inChangesToBeCommitted = 1;
+        } elsif ($inChangesToBeCommitted && $curLine =~ /^# \S/) {
+            $inChangesToBeCommitted = 0;
+        }
+    }
+
+    if (!isGit() || $curLine =~ /^#/) {
+        $logContents .= $curLine;
+    } else {
+        # $_ contains the current git log message
+        # (without the log comment info). We don't need it.
+    }
+    $existingLog = isGit() && !($curLine =~ /^#/ || $curLine =~ /^\s*$/) unless $existingLog;
+    my $changeLogFileName = changeLogFileName();
+    push @changeLogs, makeFilePathRelative($1) if $inChangesToBeCommitted && ($curLine =~ /^(?:M|A)....(.*$changeLogFileName)\r?\n?$/ || $curLine =~ /^#\t(?:modified|new file):   (.*$changeLogFileName)$/) && $curLine !~ /-$changeLogFileName$/;
+}
+close LOG;
+
+# We want to match the line endings of the existing log file in case they're
+# different from perl's line endings.
+$endl = $1 if $logContents =~ /(\r?\n)/;
+
+my $keepExistingLog = 1;
+if ($regenerateLog && $existingLog && scalar(@changeLogs) > 0 && loadTermReadKey()) {
+    print "Existing log message detected, Use 'r' to regenerate log message from ChangeLogs, or any other key to keep the existing message.\n";
+    Term::ReadKey::ReadMode('cbreak');
+    my $key = Term::ReadKey::ReadKey(0);
+    Term::ReadKey::ReadMode('normal');
+    $keepExistingLog = 0 if ($key eq "r");
+}
+
+# Don't change anything if there's already a log message (as can happen with git-commit --amend).
+exec (@editor, @ARGV) if $existingLog && $keepExistingLog;
+
+my $first = 1;
+open NEWLOG, ">$log.edit" or die;
+if (isGit() && @changeLogs == 0) {
+    # populate git commit message with WebKit-format ChangeLog entries unless explicitly disabled
+    my $branch = gitBranch();
+    chomp(my $webkitGenerateCommitMessage = `git config --bool branch.$branch.webkitGenerateCommitMessage`);
+    if ($webkitGenerateCommitMessage eq "") {
+        chomp($webkitGenerateCommitMessage = `git config --bool core.webkitGenerateCommitMessage`);
+    }
+    if ($webkitGenerateCommitMessage ne "false") {
+        open CHANGELOG_ENTRIES, "-|", "$FindBin::Bin/prepare-ChangeLog --git-index --no-write" or die "prepare-ChangeLog failed: $!.\n";
+        while (<CHANGELOG_ENTRIES>) {
+            print NEWLOG normalizeLineEndings($_, $endl);
+        }
+        close CHANGELOG_ENTRIES;
+    }
+} else {
+    print NEWLOG createCommitMessage(@changeLogs);
+}
+print NEWLOG $logContents;
+close NEWLOG;
+
+system (@editor, "$log.edit");
+
+open NEWLOG, "$log.edit" or exit;
+my $foundComment = 0;
+while (<NEWLOG>) {
+    $foundComment = 1 if (/\S/ && !/^CVS:/);
+}
+close NEWLOG;
+
+if ($foundComment) {
+    open NEWLOG, "$log.edit" or die;
+    open LOG, ">$log" or die;
+    while (<NEWLOG>) {
+        print LOG;
+    }
+    close LOG;
+    close NEWLOG;
+}
+
+unlink "$log.edit";
+
+sub createCommitMessage(@)
+{
+    my @changeLogs = @_;
+
+    my $topLevel = determineVCSRoot();
+
+    my %changeLogSort;
+    my %changeLogContents;
+    for my $changeLog (@changeLogs) {
+        open CHANGELOG, $changeLog or die "Can't open $changeLog";
+        my $contents = "";
+        my $blankLines = "";
+        my $lineCount = 0;
+        my $date = "";
+        my $author = "";
+        my $email = "";
+        my $hasAuthorInfoToWrite = 0;
+        while (<CHANGELOG>) {
+            if (/^\S/) {
+                last if $contents;
+            }
+            if (/\S/) {
+                $contents .= $blankLines if $contents;
+                $blankLines = "";
+
+                my $line = $_;
+
+                # Remove indentation spaces
+                $line =~ s/^ {8}//;
+
+                # Grab the author and the date line
+                if ($line =~ m/^([0-9]{4}-[0-9]{2}-[0-9]{2})\s+(.*[^\s])\s+<(.*)>/ && $lineCount == 0) {
+                    $date = $1;
+                    $author = $2;
+                    $email = $3;
+                    $hasAuthorInfoToWrite = 1;
+                    next;
+                }
+
+                if ($hasAuthorInfoToWrite) {
+                    my $isReviewedByLine = $line =~ m/^(?:Reviewed|Rubber[ \-]?stamped) by/;
+                    my $isModifiedFileLine = $line =~ m/^\* .*:/;
+
+                    # Insert the authorship line if needed just above the "Reviewed by" line or the
+                    # first modified file (whichever comes first).
+                    if ($isReviewedByLine || $isModifiedFileLine) {
+                        $hasAuthorInfoToWrite = 0;
+                        my $authorshipString = patchAuthorshipString($author, $email, $date);
+                        if ($authorshipString) {
+                            $contents .= "$authorshipString\n";
+                            $contents .= "\n" if $isModifiedFileLine;
+                        }
+                    }
+                }
+
+
+                $lineCount++;
+                $contents .= $line;
+            } else {
+                $blankLines .= $_;
+            }
+        }
+        if ($hasAuthorInfoToWrite) {
+            # We didn't find anywhere to put the authorship info, so just put it at the end.
+            my $authorshipString = patchAuthorshipString($author, $email, $date);
+            $contents .= "\n$authorshipString\n" if $authorshipString;
+            $hasAuthorInfoToWrite = 0;
+        }
+
+        close CHANGELOG;
+
+        $changeLog = File::Spec->abs2rel(File::Spec->rel2abs($changeLog), $topLevel);
+
+        my $label = dirname($changeLog);
+        $label = "top level" unless length $label;
+
+        my $sortKey = lc $label;
+        if ($label eq "top level") {
+            $sortKey = "";
+        } elsif ($label eq "LayoutTests") {
+            $sortKey = lc "~, LayoutTests last";
+        }
+
+        $changeLogSort{$sortKey} = $label;
+        $changeLogContents{$label} = $contents;
+    }
+
+    my $commonPrefix = removeLongestCommonPrefixEndingInDoubleNewline(%changeLogContents);
+
+    my $first = 1;
+    my @result;
+    push @result, normalizeLineEndings($commonPrefix, $endl);
+    for my $sortKey (sort keys %changeLogSort) {
+        my $label = $changeLogSort{$sortKey};
+        if (keys %changeLogSort > 1) {
+            push @result, normalizeLineEndings("\n", $endl) if !$first;
+            $first = 0;
+            push @result, normalizeLineEndings("$label: ", $endl);
+        }
+        push @result, normalizeLineEndings($changeLogContents{$label}, $endl);
+    }
+
+    return join '', @result;
+}
+
+sub loadTermReadKey()
+{
+    eval { require Term::ReadKey; };
+    return !$@;
+}
+
+sub normalizeLineEndings($$)
+{
+    my ($string, $endl) = @_;
+    $string =~ s/\r?\n/$endl/g;
+    return $string;
+}
+
+sub patchAuthorshipString($$$)
+{
+    my ($authorName, $authorEmail, $authorDate) = @_;
+
+    return if $authorEmail eq changeLogEmailAddress();
+    return "Patch by $authorName <$authorEmail> on $authorDate";
+}
+
+sub removeLongestCommonPrefixEndingInDoubleNewline(\%)
+{
+    my ($hashOfStrings) = @_;
+
+    my @strings = values %{$hashOfStrings};
+    return "" unless @strings > 1;
+
+    my $prefix = shift @strings;
+    my $prefixLength = length $prefix;
+    foreach my $string (@strings) {
+        while ($prefixLength) {
+            last if substr($string, 0, $prefixLength) eq $prefix;
+            --$prefixLength;
+            $prefix = substr($prefix, 0, -1);
+        }
+        last unless $prefixLength;
+    }
+
+    return "" unless $prefixLength;
+
+    my $lastDoubleNewline = rindex($prefix, "\n\n");
+    return "" unless $lastDoubleNewline > 0;
+
+    foreach my $key (keys %{$hashOfStrings}) {
+        $hashOfStrings->{$key} = substr($hashOfStrings->{$key}, $lastDoubleNewline);
+    }
+    return substr($prefix, 0, $lastDoubleNewline + 2);
+}
+
+sub isCommitLogEditor($)
+{
+    my $editor = shift;
+    return $editor =~ m/commit-log-editor/;
+}
diff --git a/Tools/Scripts/compare-timing-files b/Tools/Scripts/compare-timing-files
new file mode 100755
index 0000000..11b470b
--- /dev/null
+++ b/Tools/Scripts/compare-timing-files
@@ -0,0 +1,88 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2006 Apple Computer, Inc.  All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+#     its contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# This script takes two files that are lists of timings and compares them.
+
+use warnings;
+use strict;
+use Getopt::Long;
+
+my $usage = "compare-timing-files [-c|--count results] oldFile newFile";
+
+my $count = 1;
+GetOptions("c|count=i" => \$count);
+
+my ($file1, $file2) = @ARGV;
+die "$usage\n" unless ($file1 && $file2 && @ARGV == 2);
+
+my ($oldAverage, $oldRange, $oldRangePercent) = parseResults($file1);
+my ($newAverage, $newRange, $newRangePercent) = parseResults($file2);
+
+print "\n===== $file1 =====\n";
+if ($count == 1) {
+    print("fastest run: $oldAverage\n");
+} else {
+    print("average of fastest $count runs: $oldAverage\n");
+    printf("range of fastest $count runs: %.2f%% (%d)\n", $oldRangePercent, $oldRange);
+}
+
+print "\n===== $file2 =====\n";
+if ($count == 1) {
+    print("fastest run: $newAverage\n");
+} else {
+    print("average of fastest $count runs: $newAverage\n");
+    printf("range of fastest $count runs: %.2f%% (%d)\n", $newRangePercent, $newRange);
+}
+
+my $gainOrLoss = $newAverage <= $oldAverage ? "GAIN" : "LOSS";
+my $difference = abs($newAverage - $oldAverage);
+my $differencePercent = $difference / $oldAverage * 100;
+printf("\nperformance %s of %.2f%% (%.1f / %.1f)\n", $gainOrLoss, $differencePercent, $difference, $oldAverage);
+print "\n";
+
+sub parseResults
+{
+    my ($file) = @_;
+    
+    open(FILE, $file) or die "Couldn't open file: $file";
+    my @results = <FILE>;
+    close(FILE);
+
+    @results = sort(@results);
+    my $total = 0;
+    for (my $i = 0; $i < $count; $i++) {
+        $results[$i] =~ s/\D*//; # cut out non-digits
+        $total += $results[$i]; 
+    }
+    my $average = $total / $count;
+    my $range = $results[$count - 1] - $results[0];
+    my $rangePercent = $range / $results[$count - 1] * 100;
+
+    return ($average, $range, $rangePercent);
+}
+
diff --git a/Tools/Scripts/configure-github-as-upstream b/Tools/Scripts/configure-github-as-upstream
new file mode 100755
index 0000000..44336d7
--- /dev/null
+++ b/Tools/Scripts/configure-github-as-upstream
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+
+# Copyright 2012 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:
+# 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 GOOGLE INC. ``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
+# 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 script is intended to support the GitHub workflow described here:
+# https://trac.webkit.org/wiki/UsingGitHub
+#
+# This script adds WebKit's "root" repository on GitHub as a remote named
+# "upstream".  You can use sync-master-with-upstream to keep your master
+# branch in sync with WebKit's "root" repository.
+
+import subprocess
+
+exit(subprocess.call(["git", "remote", "add", "upstream", "git://github.com/WebKit/webkit.git"]))
diff --git a/Tools/Scripts/convert-test-expectations b/Tools/Scripts/convert-test-expectations
new file mode 100755
index 0000000..7ee5aa1
--- /dev/null
+++ b/Tools/Scripts/convert-test-expectations
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+
+# FIXME: Delete this file after all of the TestExpectations are converted.
+
+import sys
+
+from webkitpy.common.host import Host
+from webkitpy.layout_tests.models.test_expectations import TestExpectationParser
+
+host = Host()
+port = host.port_factory.get(sys.argv[1])
+filename = sys.argv[2]
+contents = host.filesystem.read_text_file(filename)
+
+parser = TestExpectationParser(port, [], False)
+expectation_lines = parser.parse(filename, contents)
+for line in expectation_lines:
+    new_line = line.to_string(parser._test_configuration_converter)
+    print new_line
diff --git a/Tools/Scripts/copy-webkitlibraries-to-product-directory b/Tools/Scripts/copy-webkitlibraries-to-product-directory
new file mode 100755
index 0000000..dd6f29d
--- /dev/null
+++ b/Tools/Scripts/copy-webkitlibraries-to-product-directory
@@ -0,0 +1,70 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005, 2008, 2010, 2011, 2012 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.
+
+use strict;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+my $productDir = shift @ARGV;
+$productDir = $ENV{BUILT_PRODUCTS_DIR} if !$productDir;
+
+chdirWebKit();
+
+my @librariesToCopy = (
+    "libWebKitSystemInterfaceLeopard.a",
+    "libWebKitSystemInterfaceSnowLeopard.a",
+    "libWebKitSystemInterfaceLion.a",
+    "libWebKitSystemInterfaceMountainLion.a",
+    "libWebCoreSQLite3.a",
+);
+
+my $ranlib = `xcrun -find ranlib`;
+chomp $ranlib;
+foreach my $libName (@librariesToCopy) {
+    my $srcLib = "WebKitLibraries/" . $libName;
+    my $lib = "$productDir/" . $libName;
+    if (!-e $lib || -M $lib > -M $srcLib) {
+        print "Updating $lib\n";
+        system "ditto", $srcLib, $lib;
+        system $ranlib, $lib;
+    }
+}
+
+# FIXME: This code should be abstracted to not be copy/paste.
+my $srcHeader = "WebKitLibraries/WebKitSystemInterface.h";
+my $header = "$productDir/usr/local/include/WebKitSystemInterface.h";
+if (!-e $header || -M $header > -M $srcHeader) {
+    print "Updating $header\n";
+    system "mkdir", "-p", "$productDir/usr/local/include";
+    system "ditto", $srcHeader, $header;
+}
+
+my $srcHeaderDir = "WebKitLibraries/WebCoreSQLite3";
+my $headerDir = "$productDir/WebCoreSQLite3";
+if (!-e $headerDir || -M $headerDir > -M $srcHeaderDir) {
+    print "Updating $headerDir\n";
+    system "ditto", $srcHeaderDir, $headerDir;
+}
diff --git a/Tools/Scripts/create-exports b/Tools/Scripts/create-exports
new file mode 100755
index 0000000..c645d55
--- /dev/null
+++ b/Tools/Scripts/create-exports
@@ -0,0 +1,5 @@
+#!/usr/bin/perl -w
+
+while (<>) {
+    print "$1\n" if /^\s*\"(.+)\", referenced from:$/;
+}
diff --git a/Tools/Scripts/debug-minibrowser b/Tools/Scripts/debug-minibrowser
new file mode 100755
index 0000000..052912d
--- /dev/null
+++ b/Tools/Scripts/debug-minibrowser
@@ -0,0 +1,40 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005, 2007 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 "debug" script for debugging the WebKit2 MiniBrowser.
+
+use strict;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+printHelpAndExitForRunAndDebugWebKitAppIfNeeded(INCLUDE_OPTIONS_FOR_DEBUGGING);
+
+setConfiguration();
+
+exit exitStatus(debugMiniBrowser());
diff --git a/Tools/Scripts/debug-safari b/Tools/Scripts/debug-safari
new file mode 100755
index 0000000..99273e0
--- /dev/null
+++ b/Tools/Scripts/debug-safari
@@ -0,0 +1,40 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2007 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.
+
+# Script to run Safari in the platform's debugger for the WebKit Open Source Project.
+
+use strict;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+printHelpAndExitForRunAndDebugWebKitAppIfNeeded(INCLUDE_OPTIONS_FOR_DEBUGGING);
+
+setConfiguration();
+
+exit exitStatus(debugSafari());
diff --git a/Tools/Scripts/debug-test-runner b/Tools/Scripts/debug-test-runner
new file mode 100755
index 0000000..251849f
--- /dev/null
+++ b/Tools/Scripts/debug-test-runner
@@ -0,0 +1,37 @@
+#!/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.
+
+# Simplified "debug" script for debugging the WebKitTestRunner.
+
+use strict;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+printHelpAndExitForRunAndDebugWebKitAppIfNeeded(INCLUDE_OPTIONS_FOR_DEBUGGING);
+
+setConfiguration();
+
+exit exitStatus(debugWebKitTestRunner());
diff --git a/Tools/Scripts/detect-mismatched-virtual-const b/Tools/Scripts/detect-mismatched-virtual-const
new file mode 100755
index 0000000..b345cb2
--- /dev/null
+++ b/Tools/Scripts/detect-mismatched-virtual-const
@@ -0,0 +1,167 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2008 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.
+
+#
+# This script attempts to find instances of a problem where the signatures
+# of virtual methods fail to match because one is defined 'const', and another
+# is not. For example:
+#       virtual void Base::doStuff() const;
+#       virtual void Derived::doStuff();
+#
+# The lack of 'const' on the derived class gives it a different signature, and
+# it will therefore not be called when doStuff() is called on a derived object
+# via a base class pointer.
+#
+# Limitations of this script:
+# * It only works on things in the WebCore namespace
+# * Not all templatized methods may be found correctly
+# * It doesn't know anything about inheritance, or if methods are actually virtual
+# * It has lots of false positives (should add a whitelist for known-good signatures,
+#    and specific methods)
+# * It's rather slow
+#
+# Added by Simon Fraser <simon.fraser@apple.com>
+#
+# Run the script like this:
+#  WebKitTools/Scripts/detect-mismatched-virtual-const WebKitBuild/Debug/WebCore.framework/WebCore
+#
+# Output consists of a series of warnings like this:
+#
+# Both const and non-const versions of bgColor():
+#     HTMLDocument::bgColor()
+#     HTMLBodyElement::bgColor() const
+#     HTMLTableElement::bgColor() const
+#     HTMLTableRowElement::bgColor() const
+#     HTMLTableCellElement::bgColor() const
+#
+
+use strict;
+no warnings qw /syntax/;
+
+
+my $file = $ARGV[0];
+
+print "Looking for unmatched const methods in $file\n";
+
+if (!open NM, "(nm '$file' | c++filt | sed 's/^/STDOUT:/') 2>&1 |") {
+    die "Could not open $file\n";
+}
+
+my $nestedParens;
+   $nestedParens = qr /
+                      [(]
+                      [^()]*
+                        (?:
+                          (??{ $nestedParens })
+                          [^()]*
+                        )*
+                      [)]/x;
+
+my $nestedAngleBrackets;
+   $nestedAngleBrackets = qr /
+                      [<]
+                      [^<>]*
+                        (?:
+                          (??{ $nestedAngleBrackets })
+                          [^<>]*
+                        )*
+                      [>]/x;
+
+my $bal;
+   $bal = qr /([^:]+
+              (??{ $nestedAngleBrackets })?
+              (??{ $nestedParens }))
+              ([^()]*)$/x;
+
+my %signature_map = ();
+
+while (<NM>) {
+  my $line = $_;
+  chomp($line);
+  if ($line =~ m/ [tT] WebCore::(.+)$/) {
+    my $method = $1;
+    
+    if ($method =~ /$bal/) {
+      my $signature = $1;
+      my $const = $2 eq " const";
+      
+      my $class = substr($method, 0, length($method) - length($signature) - ($const ? 6 : 0));
+
+#      print "line: $line\nclass: $class\nmethod: $method\nsignature: $signature\nconst: $const\n\n";
+
+      my %method_info = (
+          'class' => $class,
+          'const' => $const,
+          'method' => $method,
+      );
+      
+      push @{$signature_map{$signature}}, \%method_info;
+    } else {
+      print "unmatched line $method\n\n"
+    }
+  }
+}
+close NM;
+
+my $sig;
+for $sig (keys %signature_map) {
+  #print "\n$sig\n";
+
+  my @entries = @{$signature_map{$sig}};
+#  print "$#entries\n";
+  
+  my $num_const = 0;
+  my $num_not_const = 0;
+  my $i;
+  for $i (0 .. $#entries) {
+    my $entry = @entries[$i];
+
+    my $class = $entry->{'class'};
+    my $const = $entry->{'const'};
+    
+    if ($const) {
+      $num_const++;
+    } else {
+      $num_not_const++;
+    }
+  }
+
+  if ($#entries > 1 && $num_const > 0 && $num_not_const > 0) {
+    print "Both const and non-const versions of $sig:\n";
+
+    for $i (0 .. $#entries) {
+      my $entry = @entries[$i];
+      my $method = $entry->{'method'};
+      print "\t$method\n";
+    }
+
+  }
+}
+
+
+
diff --git a/Tools/Scripts/do-file-rename b/Tools/Scripts/do-file-rename
new file mode 100755
index 0000000..dbc94a0
--- /dev/null
+++ b/Tools/Scripts/do-file-rename
@@ -0,0 +1,116 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2006, 2008 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.
+
+# Script to do file renaming.
+
+use strict;
+use File::Find;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+use VCSUtils;
+
+setConfiguration();
+chdirWebKit();
+
+my %words;
+
+# find all files we want to process
+
+my @paths;
+find(\&wanted, "Source/JavaScriptCore");
+find(\&wanted, "Source/WebCore");
+find(\&wanted, "WebKit");
+find(\&wanted, "Source/WebKit2");
+
+sub wanted
+{
+    my $file = $_;
+
+    if ($file eq "icu") {
+        $File::Find::prune = 1;
+        return;
+    }
+
+    if ($file =~ /^\../) {
+        $File::Find::prune = 1;
+        return;
+    }
+
+    return if $file =~ /^ChangeLog/;
+    return if -d $file;
+
+    push @paths, $File::Find::name;
+}
+
+my %renames = (
+);
+
+my %renamesContemplatedForTheFuture = (
+);
+
+# rename files
+
+my %newFile;
+for my $file (sort @paths) {
+    my $f = $file;
+    $f = "$1$renames{$2}" if $f =~ /^(.*\/)(\w+\.\w+)$/ && $renames{$2};
+    $newFile{$file} = $f if $f ne $file;
+}
+
+for my $file (sort @paths) {
+    if ($newFile{$file}) {
+        my $newFile = $newFile{$file};
+        print "Renaming $file to $newFile\n";
+        scmMoveOrRenameFile($file, $newFile);
+    }
+}
+
+# change all file contents
+
+for my $file (sort @paths) {
+    $file = $newFile{$file} if $newFile{$file};
+    my $contents;
+    {
+        local $/;
+        open FILE, $file or die;
+        $contents = <FILE>;
+        close FILE;
+    }
+    my $newContents = $contents;
+
+    for my $from (keys %renames) {
+        $newContents =~ s/\b\Q$from\E(?!\w)/$renames{$from}/g; # this " unconfuses Xcode syntax highlighting
+    }
+
+    if ($newContents ne $contents) {
+        open FILE, ">", $file or die;
+        print FILE $newContents;
+        close FILE;
+    }
+}
diff --git a/Tools/Scripts/do-webcore-rename b/Tools/Scripts/do-webcore-rename
new file mode 100755
index 0000000..cd3cdee
--- /dev/null
+++ b/Tools/Scripts/do-webcore-rename
@@ -0,0 +1,251 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2006, 2007, 2008 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.
+
+# Script to do a rename in JavaScriptCore, WebCore, and WebKit.
+
+use strict;
+
+use File::Find;
+use FindBin;
+use Getopt::Long qw(:config pass_through);
+
+use lib $FindBin::Bin;
+use webkitdirs;
+use VCSUtils;
+
+setConfiguration();
+chdirWebKit();
+
+my $showHelp;
+my $verbose;
+
+my $programName = basename($0);
+my $usage = <<EOF;
+Usage: $programName [options]
+  -h|--help                       Show this help message
+  -v|--verbose                    More verbose output
+EOF
+
+my $getOptionsResult = GetOptions(
+    'help|h' => \$showHelp,
+    'verbose|v' => \$verbose,
+);
+
+if (!$getOptionsResult || $showHelp) {
+    print STDERR $usage;
+    exit 1;
+}
+
+my @directoriesToIgnoreList = (
+    "icu",
+);
+my %directoriesToIgnore = map { $_ => 1 } @directoriesToIgnoreList;
+
+# find all files we want to process
+
+my @paths;
+find(\&wanted, "Source/JavaScriptCore");
+find(\&wanted, "Source/WebCore");
+find(\&wanted, "Source/WebKit");
+find(\&wanted, "Source/WebKit2");
+find(\&wanted, "Tools/DumpRenderTree");
+
+sub wanted
+{
+    my $file = $_;
+
+    # Ignore excluded and hidden files/directories.
+    if ($directoriesToIgnore{$file} or $file =~ /^\../ or $file =~ /^ChangeLog/) {
+        print "Ignoring $File::Find::name\n" if $verbose;
+        $File::Find::prune = 1;
+        return;
+    }
+
+    return if -d $file;
+
+    push @paths, $File::Find::name;
+}
+
+# Setting isDOMTypeRename to 1 rather than 0 expands the regexps used
+# below to handle custom JavaScript bindings.
+my $isDOMTypeRename = 1;
+my %renames = (
+    # Renames go here in the form of:
+    "JavaScriptAudioNode" => "ScriptProcessorNode",
+    "RealtimeAnalyserNode" => "AnalyserNode",
+    "AudioGainNode" => "GainNode",
+    "AudioPannerNode" => "PannerNode",
+    "AudioChannelSplitter" => "ChannelSplitterNode",
+    "AudioChannelMerger" => "ChannelMergerNode",
+    "Oscillator" => "OscillatorNode",
+);
+
+my %renamesContemplatedForTheFuture = (
+    "HTMLPlugInImageElement" => "HTMLEmbeddedObjectElement",
+
+    "DOMObject" => "JSDOMObject",
+
+    "runtimeObjectGetter" => "pluginElementGetter",
+    "runtimeObjectPropertyGetter" => "pluginElementPropertyGetter",
+    "runtimeObjectCustomGetOwnPropertySlot" => "pluginElementCustomGetOwnPropertySlot",
+    "runtimeObjectCustomPut" => "pluginElementCustomPut",
+    "runtimeObjectImplementsCall" => "pluginElementImplementsCall",
+    "runtimeObjectCallAsFunction" => "pluginElementCallAsFunction",
+
+    "CLONE_CONTENTS" => "Clone",
+    "DELETE_CONTENTS" => "Delete",
+    "EXTRACT_CONTENTS" => "Extract",
+
+    "DateInstance" => "JSDate",
+    "ErrorInstance" => "JSError",
+
+    "KURL" => "URL",
+    "KURLCFNet" => "URLCF",
+    "KURLHash" => "URLHash",
+    "KURLMac" => "URLMac",
+    "KURL_h" => "URL_h",
+
+    "TreeShared" => "TreeRefCounted",
+
+    "StringImpl" => "SharedString",
+
+    "RenderView" => "RenderViewport",
+
+    "ObjcFallbackObjectImp" => "ObjCFallbackObject",
+    "RuntimeObjectImp" => "ForeignObject",
+
+    "runtime_array" => "BridgedArray",
+    "runtime_method" => "BridgedFunction",
+    "runtime_object" => "BridgedObject",
+    "objc_runtime" => "ObjCBridge",
+
+    "equalIgnoringCase" => "equalFoldingCase",
+
+    "FTPDirectoryTokenizer" => "FTPDirectoryDocumentBuilder",
+    "HTMLTokenizer" => "HTMLDocumentBuilder",
+    "ImageTokenizer" => "ImageDocumentBuilder",
+    "PluginTokenizer" => "PluginDocumentBuilder",
+    "TextTokenizer" => "TextDocumentBuilder",
+    "Tokenizer" => "DocumentBuilder",
+    "Tokenizer_h" => "DocumentBuilder_h",
+    "XMLTokenizer" => "XMLDocumentBuilder",
+    "isHTMLTokenizer" => "isHTMLDocumentBuilder",
+    "m_tokenizer" => "m_builder",
+    "createTokenizer" => "createBuilder",
+    "tokenizerProcessedData" => "documentBuilderProcessedData",
+
+    "WTF_UNICODE_H" => "Unicode_h",
+    "WTF_UNICODE_ICU_H" => "UnicodeICU_h",
+    "WTF_UNICODE_QT4_H" => "UnicodeQt4_h",
+    "UnicodeIcu" => "UnicodeICU",
+
+    "m_invertibleCTM" => "m_transformIsInvertible",
+
+    "NativeFunctionWrapper_h" => "JSHostFunction_h",
+    "NativeFunctionWrapper" => "JSHostFunction",
+    "nativeFunctionThunk" => "hostFunctionThunk",
+    "nativeFunction" => "hostFunction",
+    "NativeFunction" => "HostFunction",
+);
+
+# Sort the keys of the renames hash in order of decreasing length. This
+# handles the case where some of the renames are substrings of others;
+# i.e., "Foo" => "Bar" and "FooBuffer" => "BarBuffer".
+my @sortedRenameKeys = sort { length($b) - length($a) } keys %renames;
+
+# rename files
+
+sub renameFile
+{
+    my $file = shift;
+
+    if ($isDOMTypeRename) {
+        # Find the longest key in %renames which matches this more permissive regexp.
+        # (The old regexp would match ".../Foo.cpp" but not ".../JSFooCustom.cpp".)
+        # This handles renaming of custom JavaScript bindings even when some of the
+        # renames are substrings of others. The only reason we don't do this all the
+        # time is to avoid accidental file renamings for short, non-DOM renames.
+        for my $key (@sortedRenameKeys) {
+            my $newFile = "";
+            $newFile = "$1$renames{$2}$3" if $file =~ /^(.*\/\w*)($key)(\w*\.\w+)$/;
+            if ($newFile ne "") {
+                return $newFile;
+            }
+        }
+    } else {
+       $file = "$1$renames{$2}$3" if $file =~ /^(.*\/)(\w+)(\.\w+)$/ && $renames{$2};
+    }
+    return $file;
+}
+
+my %newFile;
+for my $file (sort @paths) {
+    my $f = renameFile($file);
+    if ($f ne $file) {
+        $newFile{$file} = $f;
+    }
+}
+
+for my $file (sort @paths) {
+    if ($newFile{$file}) {
+        my $newFile = $newFile{$file};
+        print "Renaming $file to $newFile\n";
+        scmMoveOrRenameFile($file, $newFile);
+    }
+}
+
+# change all file contents
+
+for my $file (sort @paths) {
+    $file = $newFile{$file} if $newFile{$file};
+    my $contents;
+    {
+        local $/;
+        open FILE, $file or die "Failed to open $file";
+        $contents = <FILE>;
+        close FILE;
+    }
+    my $newContents = $contents;
+
+    if ($isDOMTypeRename) {
+        for my $from (@sortedRenameKeys) {
+            # Handle JavaScript custom bindings.
+            $newContents =~ s/\b(JS|V8|to|)$from/$1$renames{$from}/g;
+        }
+    } else {
+        for my $from (@sortedRenameKeys) {
+            $newContents =~ s/\b$from(?!["\w])/$renames{$from}/g; # this " unconfuses Xcode syntax highlighting
+        }
+    }
+
+    if ($newContents ne $contents) {
+        open FILE, ">", $file or die "Failed to open $file";
+        print FILE $newContents;
+        close FILE;
+    }
+}
diff --git a/Tools/Scripts/ensure-valid-python b/Tools/Scripts/ensure-valid-python
new file mode 100755
index 0000000..b361641
--- /dev/null
+++ b/Tools/Scripts/ensure-valid-python
@@ -0,0 +1,70 @@
+#!/usr/bin/perl -w
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 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 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.
+
+use strict;
+
+use FindBin;
+use Getopt::Long;
+
+use lib $FindBin::Bin;
+use webkitdirs;
+use VCSUtils;
+
+sub checkPythonVersion()
+{
+    # Will exit 0 if Python is 2.5 or greater, non-zero otherwise.
+    `python -c "import sys;sys.exit(sys.version_info[:2] < (2,5))"`;
+    return exitStatus($?) == 0;
+}
+
+sub main()
+{
+    my $checkOnly = 0;
+    my $showHelp = 0;
+    my $getOptionsResult = GetOptions(
+        'check-only!' => \$checkOnly,
+        'help|h' => \$showHelp,
+    );
+    if (!$getOptionsResult || $showHelp) {
+        print STDERR <<HELP;
+Usage: $0 [options]
+  --check-only        Check python version only.
+  -h|--help           Show this help message.
+HELP
+        return 1;
+    }
+    # Congrats, your Python is fine.
+    return 0 if checkPythonVersion();
+
+    return 1 if $checkOnly;
+
+    print "Your Python version is insufficient to run WebKit's Python code.  Please update.\n";
+    print "See http://trac.webkit.org/wiki/PythonGuidelines for more info.\n";
+    return 1;
+}
+
+exit(main());
diff --git a/Tools/Scripts/execAppWithEnv b/Tools/Scripts/execAppWithEnv
new file mode 100755
index 0000000..d185e2f
--- /dev/null
+++ b/Tools/Scripts/execAppWithEnv
@@ -0,0 +1,38 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# This script launches a program with a given enviroment.
+# It is a workaround for a perl bug that apps launched from perl threads
+# use the environment of the main thread instead of the current thread.
+
+my ($unsplitEnvVar, @app) = @ARGV;
+# The first argument to this script should be perl code (in quotes) that sets the environment.
+eval substr($unsplitEnvVar, 1, -1);
+exec(@app);
diff --git a/Tools/Scripts/export-w3c-performance-wg-tests b/Tools/Scripts/export-w3c-performance-wg-tests
new file mode 100755
index 0000000..aaa8659
--- /dev/null
+++ b/Tools/Scripts/export-w3c-performance-wg-tests
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+# Copyright (C) 2012 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# This script exports newly added tests to the W3C Web Performance WG's test
+# suite.
+#
+# You must have checked out the 'webperf' repository from https://dvcs.w3.org/hg/
+#
+# This script will export the LayoutTests/http/tests/w3c/submission directory
+# to a local 'webperf' repository.
+#
+# The main step in exporting the tests is updating all of the URLs to account
+# for the differences in directory layout.
+
+import os
+import shutil
+import sys
+
+if len(sys.argv) != 3:
+    print 'USAGE: %s path_to_webkit_checkout_root path_to_webperf_checkout_root'
+    sys.exit(1)
+
+source_directory = os.path.join(sys.argv[1], 'LayoutTests', 'http', 'tests', 'w3c', 'webperf')
+destination_directory = os.path.join(sys.argv[2], 'tests')
+
+directories_to_copy = ['resources', 'submission']
+replacements = [
+        ('localhost:8000', 'www.w3c-test.org'),   # This is the alternate host for cross-server requests.
+        ('127.0.0.1:8000', 'w3c-test.org'),       # This is the primary test server.
+        ('w3c/webperf', 'webperf/tests'),         # We prepend /w3c to all of our paths.
+        ('/w3c/resources/testharness', '/resources/testharness'),
+        ('\n', '\r\n'),                           # Convert from *NIX format.
+]
+
+for directory_to_copy in directories_to_copy:
+    destination_subdirectory = os.path.join(destination_directory, directory_to_copy)
+    if not os.path.exists(destination_subdirectory):
+        os.makedirs(destination_subdirectory)
+    for root, dirs, files in os.walk(os.path.join(source_directory, directory_to_copy)):
+        print root, dirs, files
+        root = os.path.relpath(root, source_directory)
+        for dirname in dirs:
+            destination_subdirectory = os.path.join(destination_directory, root, dirname)
+            if not os.path.exists(destination_subdirectory):
+                os.makedirs(destination_subdirectory)
+        for filename in files:
+            if filename.endswith('-expected.txt'):
+                continue
+            with open(os.path.join(source_directory, root, filename), 'r') as in_file:
+                with open(os.path.join(destination_directory, root, filename), 'w') as out_file:
+                    for line in in_file:
+                        for to_find, replace_with in replacements:
+                            line = line.replace(to_find, replace_with)
+                        out_file.write(line)
diff --git a/Tools/Scripts/extract-localizable-strings b/Tools/Scripts/extract-localizable-strings
new file mode 100755
index 0000000..88f3684
--- /dev/null
+++ b/Tools/Scripts/extract-localizable-strings
@@ -0,0 +1,392 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2006, 2007, 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. 
+# 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.
+
+# This script is like the genstrings tool (minus most of the options) with these differences.
+#
+#    1) It uses the names UI_STRING and UI_STRING_WITH_KEY for the macros, rather than the macros
+#       from NSBundle.h, and doesn't support tables (although they would be easy to add).
+#    2) It supports UTF-8 in key strings (and hence uses "" strings rather than @"" strings;
+#       @"" strings only reliably support ASCII since they are decoded based on the system encoding
+#       at runtime, so give different results on US and Japanese systems for example).
+#    3) It looks for strings that are not marked for localization, using both macro names that are
+#       known to be used for debugging in Intrigue source code and an exceptions file.
+#    4) It finds the files to work on rather than taking them as parameters, and also uses a
+#       hardcoded location for both the output file and the exceptions file.
+#       It would have been nice to use the project to find the source files, but it's too hard to
+#       locate source files after parsing a .pbxproj file.
+
+# The exceptions file has a list of strings in quotes, filenames, and filename/string pairs separated by :.
+
+use strict;
+
+sub UnescapeHexSequence($);
+
+my %isDebugMacro = ( ASSERT_WITH_MESSAGE => 1, LOG_ERROR => 1, ERROR => 1, NSURL_ERROR => 1, FATAL => 1, LOG => 1, LOG_WARNING => 1, UI_STRING_LOCALIZE_LATER => 1, UI_STRING_LOCALIZE_LATER_KEY => 1, LPCTSTR_UI_STRING_LOCALIZE_LATER => 1, UNLOCALIZED_STRING => 1, UNLOCALIZED_LPCTSTR => 1, dprintf => 1, NSException => 1, NSLog => 1, printf => 1 );
+
+@ARGV >= 2 or die "Usage: extract-localizable-strings <exceptions file> <file to update> [ directory... ]\nDid you mean to run update-webkit-localizable-strings instead?\n";
+
+my $exceptionsFile = shift @ARGV;
+-f $exceptionsFile or die "Couldn't find exceptions file $exceptionsFile\n" unless $exceptionsFile eq "-";
+
+my $fileToUpdate = shift @ARGV;
+-f $fileToUpdate or die "Couldn't find file to update $fileToUpdate\n";
+
+my $warnAboutUnlocalizedStrings = $exceptionsFile ne "-";
+
+my @directories = ();
+my @directoriesToSkip = ();
+if (@ARGV < 1) {
+    push(@directories, ".");
+} else {
+    for my $dir (@ARGV) {
+        if ($dir =~ /^-(.*)$/) {
+            push @directoriesToSkip, $1;
+        } else {
+            push @directories, $dir;
+        }
+    }
+}
+
+my $sawError = 0;
+
+my $localizedCount = 0;
+my $keyCollisionCount = 0;
+my $notLocalizedCount = 0;
+my $NSLocalizeCount = 0;
+
+my %exception;
+my %usedException;
+
+if ($exceptionsFile ne "-" && open EXCEPTIONS, $exceptionsFile) {
+    while (<EXCEPTIONS>) {
+        chomp;
+        if (/^"([^\\"]|\\.)*"$/ or /^[-_\/\w\s.]+.(h|m|mm|c|cpp)$/ or /^[-_\/\w\s.]+.(h|m|mm|c|cpp):"([^\\"]|\\.)*"$/) {
+            if ($exception{$_}) {
+                print "$exceptionsFile:$.:exception for $_ appears twice\n";
+                print "$exceptionsFile:$exception{$_}:first appearance\n";
+            } else {
+                $exception{$_} = $.;
+            }
+        } else {
+            print "$exceptionsFile:$.:syntax error\n";
+        }
+    }
+    close EXCEPTIONS;
+}
+
+my $quotedDirectoriesString = '"' . join('" "', @directories) . '"';
+for my $dir (@directoriesToSkip) {
+    $quotedDirectoriesString .= ' -path "' . $dir . '" -prune -o';
+}
+
+my @files = ( split "\n", `find $quotedDirectoriesString \\( -name "*.h" -o -name "*.m" -o -name "*.mm" -o -name "*.c" -o -name "*.cpp" \\)` );
+
+for my $file (sort @files) {
+    next if $file =~ /\/\w+LocalizableStrings\w*\.h$/ || $file =~ /\/LocalizedStrings\.h$/;
+
+    $file =~ s-^./--;
+
+    open SOURCE, $file or die "can't open $file\n";
+    
+    my $inComment = 0;
+    
+    my $expected = "";
+    my $macroLine;
+    my $macro;
+    my $UIString;
+    my $key;
+    my $comment;
+    
+    my $string;
+    my $stringLine;
+    my $nestingLevel;
+    
+    my $previousToken = "";
+
+    while (<SOURCE>) {
+        chomp;
+        
+        # Handle continued multi-line comment.
+        if ($inComment) {
+            next unless s-.*\*/--;
+            $inComment = 0;
+        }
+    
+        # Handle all the tokens in the line.
+        while (s-^\s*([#\w]+|/\*|//|[^#\w/'"()\[\],]+|.)--) {
+            my $token = $1;
+            
+            if ($token eq "\"") {
+                if ($expected and $expected ne "a quoted string") {
+                    print "$file:$.:ERROR:found a quoted string but expected $expected\n";
+                    $sawError = 1;
+                    $expected = "";
+                }
+                if (s-^(([^\\$token]|\\.)*?)$token--) {
+                    if (!defined $string) {
+                        $stringLine = $.;
+                        $string = $1;
+                    } else {
+                        $string .= $1;
+                    }
+                } else {
+                    print "$file:$.:ERROR:mismatched quotes\n";
+                    $sawError = 1;
+                    $_ = "";
+                }
+                next;
+            }
+            
+            if (defined $string) {
+handleString:
+                if ($expected) {
+                    if (!defined $UIString) {
+                        # FIXME: Validate UTF-8 here?
+                        $UIString = $string;
+                        $expected = ",";
+                    } elsif (($macro =~ /(WEB_)?UI_STRING_KEY(_INTERNAL)?$/) and !defined $key) {
+                        # FIXME: Validate UTF-8 here?
+                        $key = $string;
+                        $expected = ",";
+                    } elsif (!defined $comment) {
+                        # FIXME: Validate UTF-8 here?
+                        $comment = $string;
+                        $expected = ")";
+                    }
+                } else {
+                    if (defined $nestingLevel) {
+                        # In a debug macro, no need to localize.
+                    } elsif ($previousToken eq "#include" or $previousToken eq "#import") {
+                        # File name, no need to localize.
+                    } elsif ($previousToken eq "extern" and $string eq "C") {
+                        # extern "C", no need to localize.
+                    } elsif ($string eq "") {
+                        # Empty string can sometimes be localized, but we need not complain if not.
+                    } elsif ($exception{$file}) {
+                        $usedException{$file} = 1;
+                    } elsif ($exception{"\"$string\""}) {
+                        $usedException{"\"$string\""} = 1;
+                    } elsif ($exception{"$file:\"$string\""}) {
+                        $usedException{"$file:\"$string\""} = 1;
+                    } else {
+                        print "$file:$stringLine:\"$string\" is not marked for localization\n" if $warnAboutUnlocalizedStrings;
+                        $notLocalizedCount++;
+                    }
+                }
+                $string = undef;
+                last if !defined $token;
+            }
+            
+            $previousToken = $token;
+
+            if ($token =~ /^NSLocalized/ && $token !~ /NSLocalizedDescriptionKey/ && $token !~ /NSLocalizedStringFromTableInBundle/ && $token !~ /NSLocalizedFileSizeDescription/) {
+                print "$file:$.:ERROR:found a use of an NSLocalized macro ($token); not supported\n";
+                $nestingLevel = 0 if !defined $nestingLevel;
+                $sawError = 1;
+                $NSLocalizeCount++;
+            } elsif ($token eq "/*") {
+                if (!s-^.*?\*/--) {
+                    $_ = ""; # If the comment doesn't end, discard the result of the line and set flag
+                    $inComment = 1;
+                }
+            } elsif ($token eq "//") {
+                $_ = ""; # Discard the rest of the line
+            } elsif ($token eq "'") {
+                if (!s-([^\\]|\\.)'--) { #' <-- that single quote makes the Project Builder editor less confused
+                    print "$file:$.:ERROR:mismatched single quote\n";
+                    $sawError = 1;
+                    $_ = "";
+                }
+            } else {
+                if ($expected and $expected ne $token) {
+                    print "$file:$.:ERROR:found $token but expected $expected\n";
+                    $sawError = 1;
+                    $expected = "";
+                }
+                if ($token =~ /(WEB_)?UI_STRING(_KEY)?(_INTERNAL)?$/) {
+                    $expected = "(";
+                    $macro = $token;
+                    $UIString = undef;
+                    $key = undef;
+                    $comment = undef;
+                    $macroLine = $.;
+                } elsif ($token eq "(" or $token eq "[") {
+                    ++$nestingLevel if defined $nestingLevel;
+                    $expected = "a quoted string" if $expected;
+                } elsif ($token eq ",") {
+                    $expected = "a quoted string" if $expected;
+                } elsif ($token eq ")" or $token eq "]") {
+                    $nestingLevel = undef if defined $nestingLevel && !--$nestingLevel;
+                    if ($expected) {
+                        $key = $UIString if !defined $key;
+                        HandleUIString($UIString, $key, $comment, $file, $macroLine);
+                        $macro = "";
+                        $expected = "";
+                        $localizedCount++;
+                    }
+                } elsif ($isDebugMacro{$token}) {
+                    $nestingLevel = 0 if !defined $nestingLevel;
+                }
+            }
+        }
+            
+    }
+    
+    goto handleString if defined $string;
+    
+    if ($expected) {
+        print "$file:ERROR:reached end of file but expected $expected\n";
+        $sawError = 1;
+    }
+    
+    close SOURCE;
+}
+
+# Unescapes C language hexadecimal escape sequences.
+sub UnescapeHexSequence($)
+{
+    my ($originalStr) = @_;
+
+    my $escapedStr = $originalStr;
+    my $unescapedStr = "";
+
+    for (;;) {
+        if ($escapedStr =~ s-^\\x([[:xdigit:]]+)--) {
+            if (256 <= hex($1)) {
+                print "Hexadecimal escape sequence out of range: \\x$1\n";
+                return undef;
+            }
+            $unescapedStr .= pack("H*", $1);
+        } elsif ($escapedStr =~ s-^(.)--) {
+            $unescapedStr .= $1;
+        } else {
+            return $unescapedStr;
+        }
+    }
+}
+
+my %stringByKey;
+my %commentByKey;
+my %fileByKey;
+my %lineByKey;
+
+sub HandleUIString
+{
+    my ($string, $key, $comment, $file, $line) = @_;
+
+    my $bad = 0;
+    $string = UnescapeHexSequence($string);
+    if (!defined($string)) {
+        print "$file:$line:ERROR:string has an illegal hexadecimal escape sequence\n";
+        $bad = 1;
+    }
+    $key = UnescapeHexSequence($key);
+    if (!defined($key)) {
+        print "$file:$line:ERROR:key has an illegal hexadecimal escape sequence\n";
+        $bad = 1;
+    }
+    $comment = UnescapeHexSequence($comment);
+    if (!defined($comment)) {
+        print "$file:$line:ERROR:comment has an illegal hexadecimal escape sequence\n";
+        $bad = 1;
+    }
+    if (grep { $_ == 0xFFFD } unpack "U*", $string) {
+        print "$file:$line:ERROR:string for translation has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
+        $bad = 1;
+    }
+    if ($string ne $key && grep { $_ == 0xFFFD } unpack "U*", $key) {
+        print "$file:$line:ERROR:key has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
+        $bad = 1;
+    }
+    if (grep { $_ == 0xFFFD } unpack "U*", $comment) {
+        print "$file:$line:ERROR:comment for translation has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
+        $bad = 1;
+    }
+    if ($bad) {
+        $sawError = 1;
+        return;
+    }
+    
+    if ($stringByKey{$key} && $stringByKey{$key} ne $string) {
+        print "$file:$line:encountered the same key, \"$key\", twice, with different strings\n";
+        print "$fileByKey{$key}:$lineByKey{$key}:previous occurrence\n";
+        $keyCollisionCount++;
+        return;
+    }
+    if ($commentByKey{$key} && $commentByKey{$key} ne $comment) {
+        print "$file:$line:encountered the same key, \"$key\", twice, with different comments\n";
+        print "$fileByKey{$key}:$lineByKey{$key}:previous occurrence\n";
+        $keyCollisionCount++;
+        return;
+    }
+
+    $fileByKey{$key} = $file;
+    $lineByKey{$key} = $line;
+    $stringByKey{$key} = $string;
+    $commentByKey{$key} = $comment;
+}
+
+print "\n" if $sawError || $notLocalizedCount || $NSLocalizeCount;
+
+my @unusedExceptions = sort grep { !$usedException{$_} } keys %exception;
+if (@unusedExceptions) {
+    for my $unused (@unusedExceptions) {
+        print "$exceptionsFile:$exception{$unused}:exception $unused not used\n";
+    }
+    print "\n";
+}
+
+print "$localizedCount localizable strings\n" if $localizedCount;
+print "$keyCollisionCount key collisions\n" if $keyCollisionCount;
+print "$notLocalizedCount strings not marked for localization\n" if $notLocalizedCount;
+print "$NSLocalizeCount uses of NSLocalize\n" if $NSLocalizeCount;
+print scalar(@unusedExceptions), " unused exceptions\n" if @unusedExceptions;
+
+if ($sawError) {
+    print "\nErrors encountered. Exiting without writing to $fileToUpdate.\n";
+    exit 1;
+}
+
+my $localizedStrings = "";
+
+for my $key (sort keys %commentByKey) {
+    $localizedStrings .= "/* $commentByKey{$key} */\n\"$key\" = \"$stringByKey{$key}\";\n\n";
+}
+
+# Write out the strings file in UTF-16 with a BOM.
+utf8::decode($localizedStrings) if $^V ge v5.8;
+my $output = pack "n*", (0xFEFF, unpack "U*", $localizedStrings);
+
+if (-e "$fileToUpdate") {
+    open STRINGS, ">", "$fileToUpdate" or die;
+    print STRINGS $output;
+    close STRINGS;
+} else {
+    print "$fileToUpdate does not exist\n";
+    exit 1;
+}
diff --git a/Tools/Scripts/filter-build-webkit b/Tools/Scripts/filter-build-webkit
new file mode 100755
index 0000000..97a7327
--- /dev/null
+++ b/Tools/Scripts/filter-build-webkit
@@ -0,0 +1,224 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2011 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.
+
+# Filters the output of build-webkit into a more human-readable format.
+
+use strict;
+use warnings;
+
+use CGI qw(escapeHTML);
+use File::Basename;
+use FindBin;
+use lib $FindBin::Bin;
+use Getopt::Long;
+use VCSUtils;
+
+use constant {
+    STYLE_PLAIN => 0,
+    STYLE_HEADER => 1,
+    STYLE_SUCCESS => 2,
+    STYLE_ALERT => 3,
+
+    HTML_HEADER =><<HTMLHEADER,
+<html>
+    <head>
+        <title>Build Log</title>
+        <style>
+            body { font-family: Monaco, monospace; font-size: 10px; color: #666; line-height: 1.5em; }
+            h2 { margin: 1.5em 0 0 0; font-size: 1.0em; font-weight: bold; color: blue; }
+            p { margin: 0; padding-left: 1.5em; border-left: 3px solid #fff; }
+            p.alert { border-left-color: red; color: red; margin: 1.5em 0 0 0; }
+            p.alert + p { margin: 1.5em 0 0 0; }
+            p.alert + p.alert { margin: 0; }
+            p.success { color: green; }
+        </style>
+    </head>
+    <body>
+HTMLHEADER
+
+    HTML_FOOTER =><<HTMLFOOTER,
+    </body>
+</html>
+HTMLFOOTER
+};
+
+sub printLine($$);
+sub setLogfileOption($$);
+sub setOutputFormatOption($$);
+sub usageAndExit();
+
+# Defined in VCSUtils.
+sub possiblyColored($$);
+
+my $showHelp;
+my $outputPath = "&STDOUT";
+my $outputFormat = "text";
+my $useColor = -t STDOUT;
+my $unfilteredOutputPath = "build.log";
+my $logUnfilteredOutput;
+
+sub usageAndExit()
+{
+    print STDERR <<__END__;
+Usage: @{[ basename($0) ]} [options] buildlog1 [buildlog2 ...]
+       build-webkit | @{[ basename($0) ]} [options]
+  -h|--help     Show this help message
+Output Options:
+  -o|--output   Path for output (default: STDOUT)
+  -f|--format   Output format (default: $outputFormat)
+                  text: Plain text
+                  html: Standalone HTML document
+  --[no-]color  ANSI color output for text (default: on, if -o is STDOUT)
+Unfiltered Logging Options:
+  -l|--log      Save unfiltered output to file (see --log-file)
+  --logfile     Path to save unfiltered output (implies --log, default: $unfilteredOutputPath)
+__END__
+    exit 1;
+}
+
+my $getOptionsResult = GetOptions(
+    'h|help'                => \$showHelp,
+    'o|output=s'            => \$outputPath,
+    'f|format=s'            => \&setOutputFormatOption,
+    'color!'                => \$useColor,
+    'l|log'                 => \$logUnfilteredOutput,
+    'logfile=s'             => \&setLogfileOption,
+);
+
+if (-t STDIN || $showHelp || !$getOptionsResult) {
+    usageAndExit();
+}
+
+open(OUTPUT_HANDLE, ">$outputPath") or die "Failed to open $outputPath : $!";
+if ($logUnfilteredOutput) {
+    open(UNFILTERED_OUTPUT_HANDLE, ">$unfilteredOutputPath") or die "Failed to open $unfilteredOutputPath : $!";
+}
+
+print OUTPUT_HANDLE HTML_HEADER if ($outputFormat eq "html");
+
+my $buildFinished;
+my $buildFailed = 0;
+while (my $line = <>) {
+    print UNFILTERED_OUTPUT_HANDLE $line if $logUnfilteredOutput;
+
+    chomp($line);
+
+    next if $line =~ /^\s*$/;
+    next if $line =~ /^Build settings from command line:/;
+    next if $line =~ /make: Nothing to be done for `all'\./;
+    next if $line =~ /^JavaScriptCore\/create_hash_table/;
+    next if $line =~ /JavaScriptCore.framework\/PrivateHeaders\/create_hash_table/;
+    next if $line =~ /^JavaScriptCore\/pcre\/dftables/;
+    next if $line =~ /^Creating hashtable for /;
+    next if $line =~ /^Wrote output to /;
+    next if $line =~ /^(touch|perl|cat|rm -f|bison|flex|python|\/usr\/bin\/g\+\+|gperf|echo|sed|if \[ \-f|WebCore\/generate-export-file) /;
+    next if $line =~ /^UNDOCUMENTED: /;
+    next if $line =~ /libtool.*has no symbols/;
+    next if $line =~ /^# Lower case all the values, as CSS values are case-insensitive$/;
+    next if $line =~ /^if sort /;
+    next if $line =~ /^    /;
+    next if $line =~ /^printf /;
+    next if $line =~ /^offlineasm: Nothing changed/;
+    next if $line =~ /^Showing first/;
+
+    if ($line =~ /^={10}/) {
+        printLine($line, STYLE_SUCCESS);
+        $buildFinished = 1;
+    } elsif ($line =~ /^===/) {
+        printLine($line, STYLE_HEADER);
+    } elsif ($line =~ /Checking Dependencies|Check dependencies/) {
+        printLine($line, STYLE_PLAIN);
+    } elsif ($line =~ /\*\* BUILD SUCCEEDED \*\*/) {
+        printLine("Build Succeeded", STYLE_SUCCESS);
+    } elsif ($line =~ /^(PhaseScriptExecution|CompileC|Distributed-CompileC|Ld|PBXCp|CpResource|CopyPNGFile|CopyTiffFile|CpHeader|Processing|ProcessInfoPlistFile|ProcessPCH|ProcessPCH\+\+|Touch|Libtool|CopyStringsFile|Mig|CreateUniversalBinary|Analyze|ProcessProductPackaging|CodeSign|SymLink|Updating|CompileXIB|StripNIB|CopyPlistFile|GenerateDSYMFile) ("[^"]+"|\S+)?/) {
+        my ($command, $path) = ($1, basename($2));
+        $path =~ s/"//g;
+        printLine("$command $path", STYLE_PLAIN);
+    } elsif ($line =~ /^\/\S+?(strip|WebCoreExportFileGenerator) .*?(\/|\> )(\S+)/) {
+        my ($command, $path) = (basename($1), basename($3));
+        printLine("$command $path", STYLE_PLAIN);
+    } elsif ($line =~ /^offlineasm\: /) {
+        printLine($line, STYLE_PLAIN);
+    } elsif ($line =~ /^Generating message.*(header|receiver) for (\S+)\.\.\./) {
+        my ($command, $path) = ($1, basename($2));
+        printLine("Generating message $command $path", STYLE_PLAIN);
+    } elsif ($line =~ /^(\S+\/cc).*?(\S+)\.(out|exp)/) {
+        my ($command, $path) = (basename($1), basename($2));
+        printLine("$command $path", STYLE_PLAIN);
+    } else {
+        # This only gets hit if stderr is redirected to stdout.
+        if ($line =~ /\*\* BUILD FAILED \*\*/) {
+            $buildFailed = 1;
+        }
+        printLine($line, $buildFinished ? STYLE_SUCCESS : STYLE_ALERT);
+    }
+}
+
+print OUTPUT_HANDLE HTML_FOOTER if ($outputFormat eq "html");
+
+close(OUTPUT_HANDLE);
+close(UNFILTERED_OUTPUT_HANDLE) if ($logUnfilteredOutput);
+
+exit $buildFailed;
+
+sub printLine($$)
+{
+    my ($line, $style) = @_;
+
+    if ($outputFormat eq "html") {
+        $line = escapeHTML($line);
+        if    ($style == STYLE_HEADER)  { print OUTPUT_HANDLE "<h2>$line</h2>"; }
+        elsif ($style == STYLE_SUCCESS) { print OUTPUT_HANDLE "<p class=\"success\">$line</p>"; }
+        elsif ($style == STYLE_ALERT)   { print OUTPUT_HANDLE "<p class=\"alert\">$line</p>"; }
+        else                            { print OUTPUT_HANDLE "<p>$line</p>"; }
+    } else {
+        if ($useColor) {
+            my $colors = "reset";
+            if ($style == STYLE_HEADER)  { $colors = "blue"; }
+            if ($style == STYLE_SUCCESS) { $colors = "green"; }
+            if ($style == STYLE_ALERT)   { $colors = "red"; }
+            print OUTPUT_HANDLE possiblyColored($colors, $line);
+        } else {
+            print OUTPUT_HANDLE $line;
+        }
+    }
+    print OUTPUT_HANDLE "\n";
+}
+
+sub setLogfileOption($$)
+{
+    my ($opt, $value) = @_;
+    $unfilteredOutputPath = $value;
+    $logUnfilteredOutput = 1;
+}
+
+sub setOutputFormatOption($$)
+{
+    my ($opt, $value) = @_;
+    $value = lc($value);
+    if ($value ne "html" && $value ne "text") {
+        die "The $opt option must be either \"html\" or \"text\"";
+    }
+    $outputFormat = $value;
+}
diff --git a/Tools/Scripts/find-extra-includes b/Tools/Scripts/find-extra-includes
new file mode 100755
index 0000000..4a847ed
--- /dev/null
+++ b/Tools/Scripts/find-extra-includes
@@ -0,0 +1,102 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005 Apple Computer, Inc.  All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer. 
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution. 
+# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+#     its contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission. 
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# "find-extra-includes" script for WebKit Open Source Project
+
+use strict;
+use File::Find;
+
+find(\&wanted, @ARGV ? @ARGV : ".");
+
+my %paths;
+my %includes;
+
+sub wanted
+{
+    my $file = $_;
+
+    if ($file eq "icu") {
+        $File::Find::prune = 1;
+        return;
+    }
+
+    if ($file !~ /^\./ && $file =~ /\.(h|cpp|c|mm|m)$/) {
+        $paths{$file} = $File::Find::name;
+        open FILE, $file or die;
+        while (<FILE>) {
+            if (m-^\s*#\s*(include|import)\s+["<]((\S+/)*)(\S+)[">]-) {
+                my $include = ($2 eq "sys/" ? $2 : "") . $4;
+                $includes{$file}{$include}++;
+            }
+        }
+        close FILE;
+    }
+}
+
+my %totalIncludes;
+
+sub fillOut
+{
+    my ($file) = @_;
+
+    return if defined $totalIncludes{$file};
+
+    for my $include (keys %{ $includes{$file} }) {
+        $totalIncludes{$file}{$include} = 1;
+        fillOut($include);
+        for my $i (keys %{ $totalIncludes{$include} }) {
+            $totalIncludes{$file}{$i} = 1;
+        }
+    }
+}
+
+sub check
+{
+    my ($file) = @_;
+
+    for my $include (keys %{ $includes{$file} }) {
+        fillOut($include);
+    }
+    for my $i1 (sort keys %{ $includes{$file} }) {
+        for my $i2 (keys %{ $includes{$file} }) {
+            next if $i1 eq $i2;
+            if ($totalIncludes{$i2}{$i1}) {
+                my $b1 = $i1;
+                my $b2 = $file;
+                $b1 =~ s/\..+$//;
+                $b2 =~ s/\..+$//;
+                print "$paths{$file} does not need to include $i1, because $i2 does\n" if $b1 ne $b2;
+                last;
+            }
+        }
+    }
+}
+
+for my $file (sort keys %includes) {
+    check($file);
+}
diff --git a/Tools/Scripts/find-included-framework-headers b/Tools/Scripts/find-included-framework-headers
new file mode 100755
index 0000000..759a60b
--- /dev/null
+++ b/Tools/Scripts/find-included-framework-headers
@@ -0,0 +1,30 @@
+#!/bin/sh
+# 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 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.
+#
+# A script to find headers included from the given frameworks by files in the
+# current directory (and subdirectories).
+
+for framework in $*; do
+    echo -e "\n$framework\n=================="
+    find . \( -name '*.cpp' -o -name '*.h' -o -name '*.m' -o -name '*.mm' \) -exec grep "<$framework/" {} ';' | sed -e 's|.*/\(.*\.h\).*|\1|' | sort -u
+done
diff --git a/Tools/Scripts/generate-coverage-data b/Tools/Scripts/generate-coverage-data
new file mode 100755
index 0000000..d06ee99
--- /dev/null
+++ b/Tools/Scripts/generate-coverage-data
@@ -0,0 +1,71 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005, 2006 Apple Computer, Inc.  All rights reserved.
+# Copyright (C) 2007 Holger Hans Peter Freyther.  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.
+
+# Simple script to build, run and visualize coverage information
+
+use strict;
+use File::Basename;
+use File::Spec;
+use FindBin;
+use Getopt::Long qw(:config pass_through);
+use lib $FindBin::Bin;
+use webkitdirs;
+use POSIX;
+
+# Generate a name for our results
+my $svnVersion = determineCurrentSVNRevision();
+my @timeData = localtime(time);
+my $resultName = $svnVersion . "-" . join('_', @timeData);
+my @otherOptions = ();
+
+# Move to the source directory
+# Delete old gcov files
+# Compile WebKit and run the tests
+# Generate the coverage graph...
+# Upload
+
+$ENV{'WEBKIT_COVERAGE_BUILD'} = 1;
+chdirWebKit();
+
+# Clean-up old files
+print "Cleaning up\n";
+system("if [ -d WebKitBuild ]; then find WebKitBuild -name '*.gcda' -delete; fi;") == 0 or die;
+
+
+print "Building and testing\n";
+system("Tools/Scripts/build-webkit", "--coverage", @ARGV) == 0 or die;
+system "Tools/Scripts/new-run-webkit-tests", "--no-launch-safari";
+system "Tools/Scripts/run-javascriptcore-tests", "--coverage", @ARGV;
+
+# Collect the data and generate a report
+print "Collecting coverage data\n";
+system("Tools/CodeCoverage/run-generate-coverage-data", $resultName,  "WebKitBuild/Coverage") == 0 or die;
+system("Tools/CodeCoverage/regenerate-coverage-display", "WebKitBuild/Coverage", "WebKitBuild/Coverage/html") == 0 or die;
+
+print "Done\n";
diff --git a/Tools/Scripts/generate-qt-inspector-resource b/Tools/Scripts/generate-qt-inspector-resource
new file mode 100755
index 0000000..0a8301b
--- /dev/null
+++ b/Tools/Scripts/generate-qt-inspector-resource
@@ -0,0 +1,53 @@
+#!/usr/bin/perl
+#
+# Copyright (C) 2008 Holger Hans Peter Freyther
+#
+# 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.
+
+# Regenerate WebCore/inspector/front-end/WebKit.qrc from the content of WebCore/inspector/front-end/
+
+sub addFiles(@)
+{
+    my @files = @_;
+
+    foreach $file (@files) {
+        $file =~ s,Source/WebCore/inspector/front-end/,,;
+        print WEBKIT_QRC "    <file>".$file . "</file>\n";
+    }
+}
+
+# Setup
+open(WEBKIT_QRC, ">Source/WebCore/inspector/front-end/WebKit.qrc") or die;
+print WEBKIT_QRC '<!DOCTYPE RCC><RCC version="1.0">'."\n";
+print WEBKIT_QRC '<qresource prefix="/webkit/inspector">'."\n";
+
+
+# Directory with html and js files and the images
+addFiles(<Source/WebCore/inspector/front-end/*.{*html,js,css,svg}>);
+addFiles(<Source/WebCore/inspector/front-end/Images/*>);
+
+print WEBKIT_QRC "</qresource>\n";
+print WEBKIT_QRC "</RCC>\n";
+close(WEBKIT_QRC);
diff --git a/Tools/Scripts/generate-win32-export-forwards b/Tools/Scripts/generate-win32-export-forwards
new file mode 100755
index 0000000..a160abb
--- /dev/null
+++ b/Tools/Scripts/generate-win32-export-forwards
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+
+#Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies)
+
+#This library is free software; you can redistribute it and/or
+#modify it under the terms of the GNU Library General Public
+#License as published by the Free Software Foundation; either
+#version 2 of the License, or (at your option) any later version.
+
+#This library is distributed in the hope that it will be useful,
+#but WITHOUT ANY WARRANTY; without even the implied warranty of
+#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#Library General Public License for more details.
+
+#You should have received a copy of the GNU Library General Public License
+#along with this library; see the file COPYING.LIB.  If not, write to
+#the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+#Boston, MA 02110-1301, USA.
+
+# Extract /EXPORT: linker directives from static library and write it into a
+# separate file as linker pragmas.
+# Usage: generate-win32-export-forwards \path\to\static\library.lib outputfile.cpp
+# Then compile outputfile.cpp into the final .dll and link the static library
+# into the dll.
+
+import subprocess
+import sys
+import re
+
+def exportForwardsForLibrary(library):
+    dumpBin = subprocess.Popen("dumpbin /directives " + library, stdout=subprocess.PIPE, universal_newlines=True)
+
+    output = dumpBin.communicate()[0]
+    return output
+
+libraries = sys.argv[1 : -1]
+outputFileName = sys.argv[-1]
+
+exportedSymbolRegexp = re.compile("\s*(?P<symbol>/EXPORT:.+)")
+symbols = set()
+
+for lib in libraries:
+    for line in exportForwardsForLibrary(lib).splitlines():
+        match = exportedSymbolRegexp.match(line)
+        if match:
+            symbols.add(match.group("symbol"))
+
+print("Forwarding %s symbols from %s" % (len(symbols), " ".join(libraries)))
+
+exportFile = open(outputFileName, "w")
+for symbol in symbols:
+    exportFile.write("#pragma comment(linker, \"%s\")\n" % symbol);
+exportFile.close()
diff --git a/Tools/Scripts/git-add-reviewer b/Tools/Scripts/git-add-reviewer
new file mode 100755
index 0000000..16bb06b
--- /dev/null
+++ b/Tools/Scripts/git-add-reviewer
@@ -0,0 +1,387 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) 2011 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 warnings;
+
+use File::Basename;
+use File::Temp ();
+use Getopt::Long;
+use POSIX;
+
+my $defaultReviewer = "NOBODY";
+
+sub addReviewer(\%);
+sub addReviewerToChangeLog($$$);
+sub addReviewerToCommitMessage($$$);
+sub changeLogsForCommit($);
+sub checkout($);
+sub cherryPick(\%);
+sub commit(;$);
+sub getConfigValue($);
+sub fail(;$);
+sub head();
+sub interactive();
+sub isAncestor($$);
+sub nonInteractive();
+sub rebaseOntoHead($$);
+sub requireCleanWorkTree();
+sub resetToCommit($);
+sub toCommit($);
+sub usage();
+sub writeCommitMessageToFile($);
+
+
+my $interactive = 0;
+my $showHelp = 0;
+
+my $programName = basename($0);
+my $usage = <<EOF;
+Usage: $programName -i|--interactive upstream
+       $programName commit-ish reviewer
+
+Adds a reviewer to a git commit in a repository with WebKit-style commit logs
+and ChangeLogs.
+
+When run in interactive mode, `upstream` specifies the commit after which
+reviewers should be added.
+
+When run in non-interactive mode, `commit-ish` specifies the commit to which
+the `reviewer` will be added.
+
+Options:
+  -h|--help          Display this message
+  -i|--interactive   Interactive mode
+EOF
+
+my $getOptionsResult = GetOptions(
+    'h|help' => \$showHelp,
+    'i|interactive' => \$interactive,
+);
+
+usage() if !$getOptionsResult || $showHelp;
+
+requireCleanWorkTree();
+$interactive ? interactive() : nonInteractive();
+exit;
+
+sub interactive()
+{
+    @ARGV == 1 or usage();
+
+    my $upstream = toCommit($ARGV[0]);
+    my $head = head();
+
+    isAncestor($upstream, $head) or die "$ARGV[0] is not an ancestor of HEAD.";
+
+    my @revlist = `git rev-list --reverse --pretty=oneline $upstream..`;
+    @revlist or die "Couldn't determine revisions";
+
+    my $tempFile = new File::Temp(UNLINK => 1);
+    foreach my $line (@revlist) {
+        print $tempFile "$defaultReviewer : $line";
+    }
+
+    print $tempFile <<EOF;
+
+# Change 'NOBODY' to the reviewer for each commit
+#
+# If any line starts with "rs" followed by one or more spaces, then the phrase
+# "Reviewed by" is changed to "Rubber-stamped by" in the ChangeLog(s)/commit
+# message for that commit.
+#
+# Commits may be reordered
+# Omitted commits will be lost
+EOF
+
+    close $tempFile;
+
+    my $editor = $ENV{GIT_EDITOR} || getConfigValue("core.editor") || $ENV{VISUAL} || $ENV{EDITOR} || "vi";
+    my $result = system "$editor \"" . $tempFile->filename . "\"";
+    !$result or die "Error spawning editor.";
+
+    my @todo = ();
+    open TEMPFILE, '<', $tempFile->filename or die "Error opening temp file.";
+    foreach my $line (<TEMPFILE>) {
+        next if $line =~ /^#/;
+        $line =~ /^(rs\s+)?(.*)\s+:\s+([0-9a-fA-F]+)/ or next;
+        push @todo, {rubberstamp => defined $1 && length $1, reviewer => $2, commit => $3};
+    }
+    close TEMPFILE;
+    @todo or die "No revisions specified.";
+
+    foreach my $item (@todo) {
+        $item->{changeLogs} = changeLogsForCommit($item->{commit});
+    }
+
+    $result = system "git", "checkout", $upstream;
+    !$result or die "Error checking out $ARGV[0].";
+
+    my $success = 1;
+    foreach my $item (@todo) {
+        $success = cherryPick(%{$item});
+        $success or last;
+        $success = addReviewer(%{$item});
+        $success or last;
+        $success = commit();
+        $success or last;
+    }
+
+    unless ($success) {
+        resetToCommit($head);
+        exit 1;
+    }
+
+    $result = system "git", "branch", "-f", $head;
+    !$result or die "Error updating $head.";
+    $result = system "git", "checkout", $head;
+    exit WEXITSTATUS($result);
+}
+
+sub nonInteractive()
+{
+    @ARGV == 2 or usage();
+
+    my $commit = toCommit($ARGV[0]);
+    my $reviewer = $ARGV[1];
+    my $head = head();
+    my $headCommit = toCommit($head);
+
+    isAncestor($commit, $head) or die "$ARGV[1] is not an ancestor of HEAD.";
+
+    my %item = (
+        reviewer => $reviewer,
+        commit => $commit,
+    );
+
+    $item{changeLogs} = changeLogsForCommit($commit);
+    $item{changeLogs} or die;
+
+    unless ((($commit eq $headCommit) or checkout($commit))
+            # FIXME: We need to use $ENV{GIT_DIR}/.git/MERGE_MSG
+            && writeCommitMessageToFile(".git/MERGE_MSG")
+            && addReviewer(%item)
+            && commit(1)
+            && (($commit eq $headCommit) or rebaseOntoHead($commit, $head))) {
+        resetToCommit($head);
+        exit 1;
+    }
+}
+
+sub usage()
+{
+    print STDERR $usage;
+    exit 1;
+}
+
+sub requireCleanWorkTree()
+{
+    my $result = system "git rev-parse --verify HEAD > /dev/null";
+    $result ||= system qw(git update-index --refresh);
+    $result ||= system qw(git diff-files --quiet);
+    $result ||= system qw(git diff-index --cached --quiet HEAD --);
+    !$result or die "Working tree is dirty"
+}
+
+sub fail(;$)
+{
+    my ($message) = @_;
+    print STDERR $message, "\n" if defined $message;
+    return 0;
+}
+
+sub cherryPick(\%)
+{
+    my ($item) = @_;
+
+    my $result = system "git cherry-pick -n $item->{commit} > /dev/null";
+    !$result or return fail("Failed to cherry-pick $item->{commit}");
+
+    return 1;
+}
+
+sub addReviewer(\%)
+{
+    my ($item) = @_;
+
+    return 1 if $item->{reviewer} eq $defaultReviewer;
+
+    foreach my $log (@{$item->{changeLogs}}) {
+        addReviewerToChangeLog($item->{reviewer}, $item->{rubberstamp}, $log) or return fail();
+    }
+
+    addReviewerToCommitMessage($item->{reviewer}, $item->{rubberstamp}, ".git/MERGE_MSG") or return fail();
+
+    return 1;
+}
+
+sub commit(;$)
+{
+    my ($amend) = @_;
+
+    my @command = qw(git commit -F .git/MERGE_MSG);
+    push @command, "--amend" if $amend;
+    my $result = system @command;
+    !$result or return fail("Failed to commit revision");
+
+    return 1;
+}
+
+sub addReviewerToChangeLog($$$)
+{
+    my ($reviewer, $rubberstamp, $log) = @_;
+
+    return addReviewerToFile($reviewer, $rubberstamp, $log, 0);
+}
+
+sub addReviewerToCommitMessage($$$)
+{
+    my ($reviewer, $rubberstamp, $log) = @_;
+
+    return addReviewerToFile($reviewer, $rubberstamp, $log, 1);
+}
+
+sub addReviewerToFile
+{
+    my ($reviewer, $rubberstamp, $log, $isCommitMessage) = @_;
+
+    my $tempFile = new File::Temp(UNLINK => 1);
+
+    open LOG, "<", $log or return fail("Couldn't open $log.");
+
+    my $finished = 0;
+    foreach my $line (<LOG>) {
+        if (!$finished && $line =~ /NOBODY \(OOPS!\)/) {
+            $line =~ s/NOBODY \(OOPS!\)/$reviewer/;
+            $line =~ s/Reviewed/Rubber-stamped/ if $rubberstamp;
+            $finished = 1 unless $isCommitMessage;
+        }
+
+        print $tempFile $line;
+    }
+
+    close $tempFile;
+    close LOG or return fail("Couldn't close $log");
+
+    my $result = system "mv", $tempFile->filename, $log;
+    !$result or return fail("Failed to rename $tempFile to $log");
+
+    unless ($isCommitMessage) {
+        my $result = system "git", "add", $log;
+        !$result or return fail("Failed to git add");
+    }
+
+    return 1;
+}
+
+sub head()
+{
+    my $head = `git symbolic-ref HEAD`;
+    $head =~ /^refs\/heads\/(.*)$/ or die "Couldn't determine current branch.";
+    $head = $1;
+
+    return $head;
+}
+
+sub isAncestor($$)
+{
+    my ($ancestor, $descendant) = @_;
+
+    chomp(my $mergeBase = `git merge-base $ancestor $descendant`);
+    return $mergeBase eq $ancestor;
+}
+
+sub toCommit($)
+{
+    my ($arg) = @_;
+
+    chomp(my $commit = `git rev-parse $arg`);
+    return $commit;
+}
+
+sub changeLogsForCommit($)
+{
+    my ($commit) = @_;
+
+    my @files = `git diff -r --name-status $commit^ $commit`;
+    @files or return fail("Couldn't determine changed files for $commit.");
+
+    my @changeLogs = map { /^[ACMR]\s*(.*)/; $1 } grep { /^[ACMR].*[^-]ChangeLog/ } @files;
+    return \@changeLogs;
+}
+
+sub resetToCommit($)
+{
+    my ($commit) = @_;
+
+    my $result = system "git", "checkout", "-f", $commit;
+    !$result or return fail("Error checking out $commit.");
+
+    return 1;
+}
+
+sub writeCommitMessageToFile($)
+{
+    my ($file) = @_;
+
+    open FILE, ">", $file or return fail("Couldn't open $file.");
+    open MESSAGE, "-|", qw(git rev-list --max-count=1 --pretty=format:%B HEAD) or return fail("Error running git rev-list.");
+    my $commitLine = <MESSAGE>;
+    foreach my $line (<MESSAGE>) {
+        print FILE $line;
+    }
+    close MESSAGE;
+    close FILE or return fail("Couldn't close $file.");
+
+    return 1;
+}
+
+sub rebaseOntoHead($$)
+{
+    my ($upstream, $branch) = @_;
+
+    my $result = system qw(git rebase --onto HEAD), $upstream, $branch;
+    !$result or return fail("Couldn't rebase.");
+
+    return 1;
+}
+
+sub checkout($)
+{
+    my ($commit) = @_;
+
+    my $result = system "git", "checkout", $commit;
+    !$result or return fail("Error checking out $commit.");
+
+    return 1;
+}
+
+sub getConfigValue($)
+{
+    my ($variable) = @_;
+
+    chomp(my $value = `git config --get "$variable"`);
+
+    return $value;
+}
diff --git a/Tools/Scripts/import-w3c-performance-wg-tests b/Tools/Scripts/import-w3c-performance-wg-tests
new file mode 100755
index 0000000..e48e261
--- /dev/null
+++ b/Tools/Scripts/import-w3c-performance-wg-tests
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+# Copyright (C) 2012 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# This script imports the W3C Web Performance WG's test suite into WebKit.
+#
+# You must have checked out the 'webperf' repository from https://dvcs.w3.org/hg/
+#
+# This script will populate the LayoutTests directory with the new tests. If the
+# tests already exist, the script will refuse to run. Please clear out the
+# w3c/webperf directory first.
+#
+# The main step in importing the tests is updating all of the URLs to match our
+# directory layout.
+
+import os
+import sys
+
+if len(sys.argv) != 3:
+    print 'USAGE: %s path_to_webperf_checkout_root path_to_webkit_checkout_root'
+    sys.exit(1)
+
+source_directory = os.path.join(sys.argv[1], 'tests')
+destination_directory = os.path.join(sys.argv[2], 'LayoutTests', 'http', 'tests', 'w3c', 'webperf')
+
+if os.path.exists(destination_directory):
+    print 'Refusing to overwrite existing directory: %s' % destination_directory
+    sys.exit(1)
+os.makedirs(destination_directory)
+
+directories_to_copy = ['approved', 'resources']
+directories_to_ignore = ['html5']                 # These are just duplicates of the sibling directory 'html'.
+replacements = [
+        ('www.w3c-test.org', 'localhost:8000'),   # This is the alternate host for cross-server requests.
+        ('w3c-test.org', '127.0.0.1:8000'),       # This is the primary test server.
+        ('webperf/tests', 'w3c/webperf'),         # We prepend /w3c to all of our paths.
+        ('/resources/testharness', '/w3c/resources/testharness'),
+        ('+ "(" + reloadTime[time] + ")"', ''),   # Remove dynamic values from the output. We'll still see PASS.
+        ('+ "(" + startingTime[time] + ")"', ''),
+        ('\r\n', '\n'),                           # Convert to *NIX format.
+]
+
+for directory_to_copy in directories_to_copy:
+    os.makedirs(os.path.join(destination_directory, directory_to_copy))
+    os.chdir(source_directory)
+    for root, dirs, files in os.walk(directory_to_copy):
+        for dirname in directories_to_ignore:
+            if dirname in dirs:
+                dirs.remove(dirname)
+        for dirname in dirs:
+            os.makedirs(os.path.join(destination_directory, root, dirname))
+        for filename in files:
+            with open(os.path.join(source_directory, root, filename), 'r') as in_file:
+                with open(os.path.join(destination_directory, root, filename), 'w') as out_file:
+                    for line in in_file:
+                        for to_find, replace_with in replacements:
+                            line = line.replace(to_find, replace_with)
+                        assert 'w3c-test.org' not in line, 'Imported test must not depend on live site. Bad line: "%s"' % line
+                        out_file.write(line)
diff --git a/Tools/Scripts/lint-webkitpy b/Tools/Scripts/lint-webkitpy
new file mode 100755
index 0000000..0fbbd73
--- /dev/null
+++ b/Tools/Scripts/lint-webkitpy
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import sys
+
+from webkitpy.thirdparty.autoinstalled.pylint import lint
+
+script_dir = os.path.abspath(os.path.dirname(__file__))
+if not script_dir in sys.path:
+    sys.path.append(script_dir)
+
+pylintrc = os.path.join(script_dir, 'webkitpy', 'pylintrc')
+lint.Run(['--rcfile', pylintrc, '-f', 'parseable' ] + sys.argv[1:])
diff --git a/Tools/Scripts/make-gypi b/Tools/Scripts/make-gypi
new file mode 100755
index 0000000..cebac27
--- /dev/null
+++ b/Tools/Scripts/make-gypi
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+# Copyright (c) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import re
+import sys
+
+GYPI_TEMPLATE = """{
+    'variables': {
+        '%s': [
+%s
+        ]
+    }
+}""" 
+
+def find_source_code():
+    source_code_regexp = re.compile(r'\.(cpp|h|m|mm)$')
+
+    collected_files = []
+    for directory_path, directory_names, file_names in os.walk('.'):
+        for file_name in file_names:
+            if source_code_regexp.search(file_name):
+                relative_path = os.path.join(directory_path, file_name)
+                collected_files.append(os.path.relpath(relative_path, '.'))
+
+    return collected_files
+
+def build_file_line(file_name, indent):
+    return indent +  "'%s'," % file_name
+
+def build_file_list(source_code, indent):
+    return '\n'.join([build_file_line(file_name, indent) for file_name in sorted(source_code)])
+
+def build_gypi(project):
+    key = project.lower() + '_files'
+    value = build_file_list(find_source_code(), '    ' * 3)
+    return GYPI_TEMPLATE % (key, value)
+
+def main():
+    if len(sys.argv) < 2:
+        print 'Usage: %s project_name' % sys.argv[0]
+        return
+    print build_gypi(sys.argv[1])
+
+if __name__ == "__main__":
+    main()
diff --git a/Tools/Scripts/make-new-script-test b/Tools/Scripts/make-new-script-test
new file mode 100755
index 0000000..59fbfa3
--- /dev/null
+++ b/Tools/Scripts/make-new-script-test
@@ -0,0 +1,120 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2011 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 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 create a new HTML file for a monolithic test using js-test machinery.
+
+use strict;
+
+use FindBin;
+use lib $FindBin::Bin;
+
+use File::Basename;
+use Getopt::Long;
+use webkitdirs;
+
+sub makePathToSharedSources;
+sub openTestInEditor;
+sub writeTestFile;
+
+my $showHelp;
+
+my $result = GetOptions(
+    "help"       => \$showHelp,
+);
+
+if (!$result || $showHelp || !scalar(@ARGV) || $ARGV[0] !~ m/\.html$/) {
+    print STDERR "Usage: " . basename($0) . " [-h|--help] pathname\n";
+    print STDERR "\nExamples:\n";
+    print STDERR "    " . basename($0) . " new-test.html (will create the test in current directory)\n";
+    print STDERR "    " . basename($0) . " fast/loader/new-test.html (a relative path is always from LayoutTests directory)\n";
+    print STDERR "    " . basename($0) . " /Volumes/Data/WebKit/LayoutTests/fast/loader/new-test.html\n";
+    exit 1;
+}
+
+my $providedPath = $ARGV[0];
+
+my $testAbsolutePath;
+
+# If only a file name is provided, create the test in current directory.
+$testAbsolutePath = File::Spec->rel2abs($providedPath) if (!(File::Spec->splitpath($providedPath))[1]);
+
+# Otherwise, it's either absolute, or relative to LayoutTests directory.
+chdirWebKit();
+chdir "LayoutTests";
+$testAbsolutePath = File::Spec->rel2abs($providedPath) if (!$testAbsolutePath);
+
+writeTestFile();
+
+print "$testAbsolutePath\n";
+openTestInEditor();
+
+exit 0;
+
+sub makePathToSharedSources
+{
+    my $layoutTestsPath = getcwd();
+    $testAbsolutePath =~ m/^$layoutTestsPath/ or die "Path $testAbsolutePath is not in LayoutTests directory.\n";
+    my $isHTTPTest = $testAbsolutePath =~ m/^$layoutTestsPath\/http/;
+    if ($isHTTPTest) {
+        return "/js-test-resources";
+    } else {
+        return File::Spec->abs2rel("fast/js/resources/", dirname($testAbsolutePath));
+    }
+}
+
+sub writeTestFile
+{
+    die "Test $testAbsolutePath already exists.\n" if (-e $testAbsolutePath);
+
+    my $pathToSharedSources = makePathToSharedSources();
+
+    open TEST, ">", ${testAbsolutePath} or die "Cannot create test file at $testAbsolutePath.\n";
+    print TEST << "EOF";
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<script src="$pathToSharedSources/js-test-pre.js"></script>
+</head>
+<body>
+<script>
+
+description("TEST DESCRIPTION HERE");
+
+// Your test script here. Feel free to modify surrounding HTML code if necessary.
+
+</script>
+<script src="$pathToSharedSources/js-test-post.js"></script>
+</body>
+</html>
+EOF
+    close TEST;
+}
+
+sub openTestInEditor()
+{
+    my $editor = $ENV{EDITOR};
+    exec ($editor, $testAbsolutePath) if ($editor);
+}
diff --git a/Tools/Scripts/make-script-test-wrappers b/Tools/Scripts/make-script-test-wrappers
new file mode 100755
index 0000000..74284bf
--- /dev/null
+++ b/Tools/Scripts/make-script-test-wrappers
@@ -0,0 +1,136 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2006, 2007, 2008 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.
+
+# Script to generate HTML wrappers for JavaScript tests from templates
+
+use strict;
+
+use FindBin;
+use lib $FindBin::Bin;
+
+use File::Basename;
+use File::Find;
+use Getopt::Long;
+use webkitdirs;
+
+sub directoryFilter;
+sub findTemplateFiles(@);
+
+my $showHelp;
+
+my $result = GetOptions(
+    "help"       => \$showHelp,
+);
+
+if (!$result || $showHelp) {
+    print STDERR basename($0) . " [-h|--help]\n";
+    exit 1;
+}
+
+setConfiguration();
+my $productDir = productDir();
+
+chdirWebKit();
+
+my @templates = findTemplateFiles(@ARGV);
+
+for my $tfile (@templates) {
+
+    my $tpath = $tfile;
+    my $templateDirectory;
+    my $templateRelativePath;
+    if ($tpath =~ s:/(script-tests)/TEMPLATE.html$::) {
+        $templateDirectory = $1;
+        $templateRelativePath = $1 . "/TEMPLATE.html";
+    } else {
+        print "Inappropriate position of a template: ${tpath}\n";
+        next;
+    }
+
+    print "${tpath}\n";
+
+    chdirWebKit();
+    chdir($tpath);
+
+    my @files;
+    my $fileFilter = sub {
+        push @files, $File::Find::name if substr($_, -3) eq ".js";
+    };
+    find({ preprocess => \&directoryFilter, wanted => $fileFilter }, $templateDirectory);
+
+    open TEMPLATE, "<${templateRelativePath}";
+    my $template = do { local $/; <TEMPLATE> };
+    close TEMPLATE;
+
+    my $templateNegative = $template;
+    if (-e "${templateDirectory}/TEMPLATE-n.html") {
+        open TEMPLATE, "<${templateDirectory}/TEMPLATE-n.html";
+        $templateNegative = do { local $/; <TEMPLATE> };
+        close TEMPLATE;
+    }
+
+    for my $file (@files) {
+        my $html = $file;
+        $html =~ s:${templateDirectory}/(.*)\.js:$1.html:;
+        next if -f "$html-disabled";
+
+        system("cat ${file} | tr '\\0' ' ' | grep -q 'successfullyParsed ='");
+        if ($? != 0) {
+            `echo "" >> "${file}"`;
+            `echo "var successfullyParsed = true;" >> "${file}"`;
+        }
+        
+        print "    ${html}\n";
+        open HTML, ">$html";
+        my $output = ($file =~ /-n\.js/) ? $templateNegative : $template;
+        $output =~ s:YOUR_JS_FILE_HERE:$file:;
+        print HTML $output;
+        
+        close HTML;
+    }
+}
+
+exit 0;
+
+sub directoryFilter
+{
+    return () if basename($File::Find::dir) eq ".svn";
+    return @_;
+}
+
+sub findTemplateFiles(@) {
+    my @templateFiles;
+
+    my $fileFilter = sub {
+        push @templateFiles, $File::Find::name if $_ eq "TEMPLATE.html";
+    };
+
+    find({ preprocess => \&directoryFilter, wanted => $fileFilter }, "LayoutTests/fast/js");
+
+    return @templateFiles;
+}
diff --git a/Tools/Scripts/malloc-tree b/Tools/Scripts/malloc-tree
new file mode 100755
index 0000000..41769cb
--- /dev/null
+++ b/Tools/Scripts/malloc-tree
@@ -0,0 +1,210 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2011 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.
+
+import sys
+import getopt
+from optparse import OptionParser
+
+oneK = 1024
+oneM = 1024 * 1024
+oneG = 1024 * 1024 * 1024
+
+hotspot = False
+scaleSize = True
+showBars = True
+
+def byteString(bytes):
+    if scaleSize:
+        format = '  %4d   '
+        val = bytes
+
+        if bytes >= oneG:
+            format = '%8.1fG'
+            val = float(bytes) / oneG
+        elif bytes >= oneM:
+            format = '%8.1fM'
+            val = float(bytes) / oneM
+        elif bytes >= oneK:
+            format = '%8.1fK'
+            val = float(bytes) / oneK
+
+        return format % val
+    if hotspot:
+        return '%d' % bytes
+    return '%12d' % bytes
+
+class Node:
+    def __init__(self, name, level = 0, bytes = 0):
+        self.name = name
+        self.level = level
+        self.children = {}
+        self.totalBytes = bytes
+
+    def hasChildren(self):
+        return len(self.children) > 0
+
+    def getChild(self, name):
+        if not name in self.children:
+            newChild = Node(name, self.level + 1)
+            self.children[name] = newChild
+
+        return self.children[name]
+
+    def getBytes(self):
+        return self.totalBytes
+
+    def addBytes(self, bytes):
+        self.totalBytes = self.totalBytes + bytes
+
+    def processLine(self, bytes, line):
+        sep = line.find('|')
+        if sep < 0:
+            childName = line.strip()
+            line = ''
+        else:
+            childName = line[:sep].strip()
+            line = line[sep+1:]
+
+        child = self.getChild(childName)
+        child.addBytes(bytes)
+
+        if len(line) > 0:
+            child.processLine(bytes, line)
+
+    def printNode(self, prefix = ' '):
+        global hotspot
+        global scaleSize
+        global showBars
+
+        if self.hasChildren():
+            byteStr = byteString(self.totalBytes)
+
+            if hotspot:
+                print('    %s%s %s' % (self.level * ' ', byteString(self.totalBytes), self.name))
+            else:
+                print('%s %s%s' % (byteString(self.totalBytes), prefix[:-1], self.name))
+
+            sortedChildren = sorted(self.children.values(), key=sortKeyByBytes, reverse=True)
+
+            if showBars and len(self.children) > 1:
+                newPrefix = prefix + '|'
+            else:
+                newPrefix = prefix + ' '
+
+            childrenLeft = len(sortedChildren)
+            for child in sortedChildren:
+                if childrenLeft <= 1:
+                    newPrefix = prefix + ' '
+                else:
+                    childrenLeft = childrenLeft - 1
+                child.printNode(newPrefix)
+        else:
+            byteStr = byteString(self.totalBytes)
+
+            if hotspot:
+                print('    %s%s %s' % (self.level * ' ', byteString(self.totalBytes), self.name))
+            else:
+                print('%s %s%s' % (byteString(self.totalBytes), prefix[:-1], self.name))
+
+def sortKeyByBytes(node):
+    return node.getBytes();
+
+def main():
+    global hotspot
+    global scaleSize
+    global showBars
+
+    # parse command line options
+    parser = OptionParser(usage='malloc-tree [options] [malloc_history-file]',
+                          description='Format malloc_history output as a nested tree',
+                          epilog='stdin used if malloc_history-file is missing')
+
+    parser.add_option('-n', '--nobars', action='store_false', dest='showBars',
+                      default=True, help='don\'t show bars lining up siblings in tree');
+    parser.add_option('-b', '--size-in-bytes', action='store_false', dest='scaleSize',
+                      default=None, help='show sizes in bytes');
+    parser.add_option('-s', '--size-scale', action='store_true', dest='scaleSize',
+                      default=None, help='show sizes with appropriate scale suffix [K,M,G]');
+    parser.add_option('-t', '--hotspot', action='store_true', dest='hotspot',
+                      default=False, help='output in HotSpotFinder format, implies -b');
+
+    (options, args) = parser.parse_args()
+
+    hotspot = options.hotspot
+    if options.scaleSize is None:
+        if hotspot:
+            scaleSize = False
+        else:
+            scaleSize = True
+    else:
+        scaleSize = options.scaleSize
+    showBars = options.showBars
+
+    if len(args) < 1:
+        inputFile = sys.stdin
+    else:
+        inputFile = open(args[0], "r")
+
+    line = inputFile.readline()
+ 
+    rootNodes = {}
+
+    while line:
+        firstSep = line.find('|')
+        if firstSep > 0:
+            firstPart = line[:firstSep].strip()
+            lineRemain = line[firstSep+1:]
+            bytesSep = firstPart.find('bytes:')
+            if bytesSep >= 0:
+                name = firstPart[bytesSep+7:]
+                stats = firstPart.split(' ')
+                bytes = int(stats[3].replace(',', ''))
+
+                if not name in rootNodes:
+                    node = Node(name, 0, bytes);
+                    rootNodes[name] = node
+                else:
+                    node = rootNodes[name]
+                    node.addBytes(bytes)
+
+                node.processLine(bytes, lineRemain)
+
+        line = inputFile.readline()
+
+    sortedRootNodes = sorted(rootNodes.values(), key=sortKeyByBytes, reverse=True)
+
+    print 'Call graph:'
+    try:
+        for node in sortedRootNodes:
+            node.printNode()
+            print 
+    except:
+        pass
+
+if __name__ == "__main__":
+    main()
diff --git a/Tools/Scripts/new-run-webkit-httpd b/Tools/Scripts/new-run-webkit-httpd
new file mode 100755
index 0000000..9ace3e1
--- /dev/null
+++ b/Tools/Scripts/new-run-webkit-httpd
@@ -0,0 +1,99 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""A utility script for starting and stopping the HTTP server with the
+   same configuration as used in the layout tests."""
+
+#
+# FIXME: currently this code only works with the Chromium ports and LigHTTPd.
+# It should be made to work on all ports.
+#
+# This script is also used by Chromium's ui_tests to run http layout tests
+# in a browser.
+#
+import optparse
+import os
+import sys
+import tempfile
+
+import webkitpy.common.version_check
+
+from webkitpy.common.host import Host
+from webkitpy.layout_tests.servers import http_server
+
+
+def run(options):
+    if not options.server:
+        print ('Usage: %s --server {start|stop} [--root=root_dir]'
+               ' [--port=port_number]' % sys.argv[0])
+    else:
+        if (options.root is None) and (options.port is not None):
+            # specifying root but not port means we want httpd on default
+            # set of ports that LayoutTest use, but pointing to a different
+            # source of tests. Specifying port but no root does not seem
+            # meaningful.
+            raise 'Specifying port requires also a root.'
+        host = Host()
+        # FIXME: Make this work with other ports as well.
+        port_obj = host.port_factory.get(port_name='chromium', options=options)
+        httpd = http_server.Lighttpd(port_obj,
+                                     tempfile.gettempdir(),
+                                     port=options.port,
+                                     root=options.root,
+                                     run_background=options.run_background,
+                                     layout_tests_dir=options.layout_tests_dir)
+        if options.server == 'start':
+            httpd.start()
+        else:
+            httpd.stop()
+
+
+def main():
+    option_parser = optparse.OptionParser()
+    option_parser.add_option('-k', '--server',
+        help='Server action (start|stop)')
+    option_parser.add_option('-p', '--port',
+        help='Port to listen on (overrides layout test ports)')
+    option_parser.add_option('-r', '--root',
+        help='Absolute path to DocumentRoot (overrides layout test roots)')
+    option_parser.add_option('--register_cygwin', action="store_true",
+        dest="register_cygwin", help='Register Cygwin paths (on Win try bots)')
+    option_parser.add_option('--run_background', action="store_true",
+        dest="run_background",
+        help='Run on background (for running as UI test)')
+    option_parser.add_option('--layout_tests_dir',
+        dest="layout_tests_dir",
+        help='Absolute path to LayoutTests root')
+    options, args = option_parser.parse_args()
+
+    run(options)
+
+
+if '__main__' == __name__:
+    main()
diff --git a/Tools/Scripts/new-run-webkit-tests b/Tools/Scripts/new-run-webkit-tests
new file mode 100755
index 0000000..6b1a98b
--- /dev/null
+++ b/Tools/Scripts/new-run-webkit-tests
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+# Copyright (C) 2011 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.
+
+"""Wrapper around webkitpy/layout_tests/run_webkit_tests.py"""
+from webkitpy.common import multiprocessing_bootstrap
+
+multiprocessing_bootstrap.run('webkitpy', 'layout_tests', 'run_webkit_tests.py')
diff --git a/Tools/Scripts/new-run-webkit-websocketserver b/Tools/Scripts/new-run-webkit-websocketserver
new file mode 100755
index 0000000..15ed1f9
--- /dev/null
+++ b/Tools/Scripts/new-run-webkit-websocketserver
@@ -0,0 +1,115 @@
+#!/usr/bin/env python
+# Copyright (C) 2012 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 utility script for starting and stopping the web socket server with the
+   same configuration as used in the layout tests."""
+
+import logging
+import optparse
+import tempfile
+
+from webkitpy.common.host import Host
+from webkitpy.layout_tests.servers import websocket_server
+
+
+def main():
+    option_parser = optparse.OptionParser()
+    option_parser.add_option('--server', type='choice',
+                             choices=['start', 'stop'], default='start',
+                             help='Server action (start|stop).')
+    option_parser.add_option('-p', '--port', dest='port',
+                             default=None, help='Port to listen on.')
+    option_parser.add_option('-r', '--root',
+                             help='Absolute path to DocumentRoot '
+                                  '(overrides layout test roots).')
+    option_parser.add_option('-t', '--tls', dest='use_tls',
+                             action='store_true',
+                             default=False, help='use TLS (wss://).')
+    option_parser.add_option('-k', '--private_key', dest='private_key',
+                             default='', help='TLS private key file.')
+    option_parser.add_option('-c', '--certificate', dest='certificate',
+                             default='', help='TLS certificate file.')
+    option_parser.add_option('--ca-certificate', dest='ca_certificate',
+                             default='', help='TLS CA certificate file for '
+                                              'client authentication.')
+    option_parser.add_option('--chromium', action='store_true',
+                             dest='chromium',
+                             default=False,
+                             help='Use the Chromium port.')
+    option_parser.add_option('--register_cygwin', action="store_true",
+                             dest="register_cygwin",
+                             help='Register Cygwin paths (on Win try bots).')
+    option_parser.add_option('--pidfile', help='path to pid file.')
+    option_parser.add_option('--output-dir', dest='output_dir',
+                             default=None, help='output directory.')
+    option_parser.add_option('-v', '--verbose', action='store_true',
+                             default=False,
+                             help='Include debug-level logging.')
+    options, args = option_parser.parse_args()
+
+    if not options.port:
+        if options.use_tls:
+            # FIXME: We shouldn't grab at this private variable.
+            options.port = websocket_server._DEFAULT_WSS_PORT
+        else:
+            # FIXME: We shouldn't grab at this private variable.
+            options.port = websocket_server._DEFAULT_WS_PORT
+
+    if not options.output_dir:
+        options.output_dir = tempfile.gettempdir()
+
+    kwds = {'port': options.port, 'use_tls': options.use_tls}
+    if options.root:
+        kwds['root'] = options.root
+    if options.private_key:
+        kwds['private_key'] = options.private_key
+    if options.certificate:
+        kwds['certificate'] = options.certificate
+    if options.ca_certificate:
+        kwds['ca_certificate'] = options.ca_certificate
+    if options.pidfile:
+        kwds['pidfile'] = options.pidfile
+
+    host = Host()
+    # FIXME: Make this work with other ports as well.
+    port_obj = host.port_factory.get(port_name='chromium', options=options)
+    pywebsocket = websocket_server.PyWebSocket(port_obj, options.output_dir, **kwds)
+
+    log_level = logging.WARN
+    if options.verbose:
+        log_level = logging.DEBUG
+    logging.basicConfig(level=log_level)
+
+    if 'start' == options.server:
+        pywebsocket.start()
+    else:
+        pywebsocket.stop()
+
+if '__main__' == __name__:
+    main()
diff --git a/Tools/Scripts/num-cpus b/Tools/Scripts/num-cpus
new file mode 100755
index 0000000..8a8c97f
--- /dev/null
+++ b/Tools/Scripts/num-cpus
@@ -0,0 +1,6 @@
+#!/usr/bin/perl -w
+use strict;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+print numberOfCPUs() . "\n";
diff --git a/Tools/Scripts/old-run-webkit-tests b/Tools/Scripts/old-run-webkit-tests
new file mode 100755
index 0000000..ef3b197
--- /dev/null
+++ b/Tools/Scripts/old-run-webkit-tests
@@ -0,0 +1,2876 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights reserved.
+# Copyright (C) 2006 Alexey Proskuryakov (ap@nypop.com)
+# Copyright (C) 2007 Matt Lilek (pewtermoose@gmail.com)
+# Copyright (C) 2007 Eric Seidel <eric@webkit.org>
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2009 Andras Becsi (becsi.andras@stud.u-szeged.hu), University of Szeged
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer. 
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution. 
+# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+#     its contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission. 
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Script to run the WebKit Open Source Project layout tests.
+
+# Run all the tests passed in on the command line.
+# If no tests are passed, find all the .html, .shtml, .xml, .xhtml, .xhtmlmp, .pl, .php (and svg) files in the test directory.
+
+# Run each text.
+# Compare against the existing file xxx-expected.txt.
+# If there is a mismatch, generate xxx-actual.txt and xxx-diffs.txt.
+
+# At the end, report:
+#   the number of tests that got the expected results
+#   the number of tests that ran, but did not get the expected results
+#   the number of tests that failed to run
+#   the number of tests that were run but had no expected results to compare against
+
+use strict;
+use warnings;
+
+use CGI;
+use Config;
+use Cwd;
+use Data::Dumper;
+use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK);
+use File::Basename;
+use File::Copy;
+use File::Find;
+use File::Path;
+use File::Spec;
+use File::Spec::Functions;
+use File::Temp;
+use FindBin;
+use Getopt::Long;
+use IPC::Open2;
+use IPC::Open3;
+use MIME::Base64;
+use Time::HiRes qw(time usleep);
+
+use List::Util 'shuffle';
+
+use lib $FindBin::Bin;
+use webkitperl::features;
+use webkitperl::httpd;
+use webkitdirs;
+use VCSUtils;
+use POSIX;
+
+sub buildPlatformResultHierarchy();
+sub buildPlatformTestHierarchy(@);
+sub captureSavedCrashLog($$);
+sub checkPythonVersion();
+sub closeCygpaths();
+sub closeDumpTool();
+sub closeWebSocketServer();
+sub configureAndOpenHTTPDIfNeeded();
+sub countAndPrintLeaks($$$);
+sub countFinishedTest($$$$);
+sub deleteExpectedAndActualResults($);
+sub dumpToolDidCrash();
+sub epiloguesAndPrologues($$);
+sub expectedDirectoryForTest($;$;$);
+sub fileNameWithNumber($$);
+sub findNewestFileMatchingGlob($);
+sub htmlForResultsSection(\@$&);
+sub isTextOnlyTest($);
+sub launchWithEnv(\@\%);
+sub resolveAndMakeTestResultsDirectory();
+sub numericcmp($$);
+sub openDiffTool();
+sub buildDumpTool($);
+sub openDumpTool();
+sub parseLeaksandPrintUniqueLeaks();
+sub openWebSocketServerIfNeeded();
+sub pathcmp($$);
+sub printFailureMessageForTest($$);
+sub processIgnoreTests($$);
+sub processSkippedFileEntry($$$);
+sub readChecksumFromPng($);
+sub readFromDumpToolWithTimer(**);
+sub readSkippedFiles($);
+sub recordActualResultsAndDiff($$);
+sub sampleDumpTool();
+sub setFileHandleNonBlocking(*$);
+sub setUpWindowsCrashLogSaving();
+sub slowestcmp($$);
+sub splitpath($);
+sub startsWith($$);
+sub stopRunningTestsEarlyIfNeeded();
+sub stripExtension($);
+sub stripMetrics($$);
+sub testCrashedOrTimedOut($$$$$$);
+sub toCygwinPath($);
+sub toURL($);
+sub toWindowsPath($);
+sub validateSkippedArg($$;$);
+sub writeToFile($$);
+
+# Argument handling
+my $addPlatformExceptions = 0;
+my @additionalPlatformDirectories = ();
+my $complexText = 0;
+my $exitAfterNFailures = 0;
+my $exitAfterNCrashesOrTimeouts = 0;
+my $generateNewResults = isAppleMacWebKit() ? 1 : 0;
+my $guardMalloc = '';
+# FIXME: Dynamic HTTP-port configuration in this file is wrong.  The various
+# apache config files in LayoutTests/http/config govern the port numbers.
+# Dynamic configuration as-written will also cause random failures in
+# an IPv6 environment.  See https://bugs.webkit.org/show_bug.cgi?id=37104.
+my $httpdPort = 8000;
+my $httpdSSLPort = 8443;
+my $ignoreMetrics = 0;
+my $webSocketPort = 8880;
+# wss is disabled until all platforms support pyOpenSSL.
+# my $webSocketSecurePort = 9323;
+my @ignoreTests;
+my $iterations = 1;
+my $launchSafari = 1;
+my $mergeDepth;
+my $pixelTests = '';
+my $platform;
+my $quiet = '';
+my $randomizeTests = 0;
+my $repeatEach = 1;
+my $report10Slowest = 0;
+my $resetResults = 0;
+my $reverseTests = 0;
+my $root;
+my $runSample = 1;
+my $shouldCheckLeaks = 0;
+my $showHelp = 0;
+my $stripEditingCallbacks;
+my $testHTTP = 1;
+my $testWebSocket = 1;
+my $testMedia = 1;
+my $tmpDir = "/tmp";
+my $testResultsDirectory = File::Spec->catdir($tmpDir, "layout-test-results");
+my $testsPerDumpTool = 1000;
+my $threaded = 0;
+my $gcBetweenTests = 0;
+# DumpRenderTree has an internal timeout of 30 seconds, so this must be > 30.
+my $timeoutSeconds = 35;
+my $tolerance = 0;
+my $treatSkipped = "default";
+my $useRemoteLinksToTests = 0;
+my $useValgrind = 0;
+my $verbose = 0;
+my $shouldWaitForHTTPD = 0;
+my $useWebKitTestRunner = 0;
+my $noBuildDumpTool = 0;
+
+# These arguments are ignored, but exist for compatibility with new-run-webkit-tests.
+my $builderName = '';
+my $buildNumber = '';
+my $masterName = '';
+my $testResultsServer = '';
+
+my @leaksFilenames;
+
+if (isWindows()) {
+    print "This script has to be run under Cygwin to function correctly.\n";
+    exit 1;
+}
+
+# Default to --no-http for wx for now.
+$testHTTP = 0 if (isWx());
+
+my $perlInterpreter = "perl";
+
+my $expectedTag = "expected";
+my $mismatchTag = "mismatch";
+my $actualTag = "actual";
+my $prettyDiffTag = "pretty-diff";
+my $diffsTag = "diffs";
+my $errorTag = "stderr";
+my $crashLogTag = "crash-log";
+
+my $windowsCrashLogFilePrefix = "CrashLog";
+
+# These are defined here instead of closer to where they are used so that they
+# will always be accessible from the END block that uses them, even if the user
+# presses Ctrl-C before Perl has finished evaluating this whole file.
+my $windowsPostMortemDebuggerKey = "/HKLM/SOFTWARE/Microsoft/Windows NT/CurrentVersion/AeDebug";
+my %previousWindowsPostMortemDebuggerValues;
+
+my $realPlatform;
+
+my @macPlatforms = ("mac-leopard", "mac-snowleopard", "mac-lion", "mac");
+my @winPlatforms = ("win-xp", "win-vista", "win-7sp0", "win");
+
+if (isAppleMacWebKit()) {
+    if (isSnowLeopard()) {
+        $platform = "mac-snowleopard";
+        $tolerance = 0.1;
+    } elsif (isLion()) {
+        $platform = "mac-lion";
+        $tolerance = 0.1;
+    } else {
+        $platform = "mac";
+    }
+} elsif (isQt()) {
+    $platform = "qt";
+} elsif (isGtk()) {
+    $platform = "gtk";
+} elsif (isWx()) {
+    $platform = "wx";
+} elsif (isWinCairo()) {
+    $platform = "wincairo";
+} elsif (isCygwin() || isWindows()) {
+    if (isWindowsXP()) {
+        $platform = "win-xp";
+    } elsif (isWindowsVista()) {
+        $platform = "win-vista";
+    } elsif (isWindows7SP0()) {
+        $platform = "win-7sp0";
+    } else {
+        $platform = "win";
+    }
+}
+
+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";
+        print "You must set it before running the tests.\n";
+        print "Use git to grab the actual fonts from http://gitorious.org/qtwebkit/testfonts\n";
+        exit 1;
+    }
+}
+
+if (!defined($platform)) {
+    print "WARNING: Your platform is not recognized. Any platform-specific results will be generated in platform/undefined.\n";
+    $platform = "undefined";
+}
+
+if (!checkPythonVersion()) {
+    print "WARNING: Your platform does not have Python 2.5+, which is required to run websocket server, so disabling websocket/tests.\n";
+    $testWebSocket = 0;
+}
+
+my $programName = basename($0);
+my $launchSafariDefault = $launchSafari ? "launch" : "do not launch";
+my $httpDefault = $testHTTP ? "run" : "do not run";
+my $sampleDefault = $runSample ? "run" : "do not run";
+
+my $usage = <<EOF;
+Usage: $programName [options] [testdir|testpath ...]
+  --add-platform-exceptions       Put new results for non-platform-specific failing tests into the platform-specific results directory
+  --additional-platform-directory path/to/directory
+                                  Look in the specified directory before looking in any of the default platform-specific directories
+  --complex-text                  Use the complex text code path for all text (Mac OS X and Windows only)
+  -c|--configuration config       Set DumpRenderTree build configuration
+  --gc-between-tests              Force garbage collection between each test
+  -g|--guard-malloc               Enable Guard Malloc
+  --exit-after-n-failures N       Exit after the first N failures (includes crashes) instead of running all tests
+  --exit-after-n-crashes-or-timeouts N
+                                  Exit after the first N crashes instead of running all tests
+  -h|--help                       Show this help message
+  --[no-]http                     Run (or do not run) http tests (default: $httpDefault)
+  --[no-]wait-for-httpd           Wait for httpd if some other test session is using it already (same as WEBKIT_WAIT_FOR_HTTPD=1). (default: $shouldWaitForHTTPD) 
+  -i|--ignore-tests               Comma-separated list of directories or tests to ignore
+  --iterations n                  Number of times to run the set of tests (e.g. ABCABCABC)
+  --[no-]launch-safari            Launch (or do not launch) Safari to display test results (default: $launchSafariDefault)
+  -l|--leaks                      Enable leaks checking
+  --[no-]new-test-results         Generate results for new tests
+  --nthly n                       Restart DumpRenderTree every n tests (default: $testsPerDumpTool)
+  -p|--pixel-tests                Enable pixel tests
+  --tolerance t                   Ignore image differences less than this percentage (default: $tolerance)
+  --platform                      Override the detected platform to use for tests and results (default: $platform)
+  --port                          Web server port to use with http tests
+  -q|--quiet                      Less verbose output
+  --reset-results                 Reset ALL results (including pixel tests if --pixel-tests is set)
+  -o|--results-directory          Output results directory (default: $testResultsDirectory)
+  --random                        Run the tests in a random order
+  --repeat-each n                 Number of times to run each test (e.g. AAABBBCCC)
+  --reverse                       Run the tests in reverse alphabetical order
+  --root                          Path to root tools build
+  --[no-]sample-on-timeout        Run sample on timeout (default: $sampleDefault) (Mac OS X only)
+  -1|--singly                     Isolate each test case run (implies --nthly 1 --verbose)
+  --skipped=[default|ignore|only] Specifies how to treat the Skipped file
+                                     default: Tests/directories listed in the Skipped file are not tested
+                                     ignore:  The Skipped file is ignored
+                                     only:    Only those tests/directories listed in the Skipped file will be run
+  --slowest                       Report the 10 slowest tests
+  --ignore-metrics                Ignore metrics in tests
+  --[no-]strip-editing-callbacks  Remove editing callbacks from expected results
+  -t|--threaded                   Run a concurrent JavaScript thead with each test
+  --timeout t                     Sets the number of seconds before a test times out (default: $timeoutSeconds)
+  --valgrind                      Run DumpRenderTree inside valgrind (Qt/Linux only)
+  -v|--verbose                    More verbose output (overrides --quiet)
+  -m|--merge-leak-depth arg       Merges leak callStacks and prints the number of unique leaks beneath a callstack depth of arg.  Defaults to 5.
+  --use-remote-links-to-tests     Link to test files within the SVN repository in the results.
+  -2|--webkit-test-runner         Use WebKitTestRunner rather than DumpRenderTree.
+EOF
+
+setConfiguration();
+
+my $getOptionsResult = GetOptions(
+    'add-platform-exceptions' => \$addPlatformExceptions,
+    'additional-platform-directory=s' => \@additionalPlatformDirectories,
+    'complex-text' => \$complexText,
+    'exit-after-n-failures=i' => \$exitAfterNFailures,
+    'exit-after-n-crashes-or-timeouts=i' => \$exitAfterNCrashesOrTimeouts,
+    'gc-between-tests' => \$gcBetweenTests,
+    'guard-malloc|g' => \$guardMalloc,
+    'help|h' => \$showHelp,
+    'http!' => \$testHTTP,
+    'wait-for-httpd!' => \$shouldWaitForHTTPD,
+    'ignore-metrics!' => \$ignoreMetrics,
+    'ignore-tests|i=s' => \@ignoreTests,
+    'iterations=i' => \$iterations,
+    'launch-safari!' => \$launchSafari,
+    'leaks|l' => \$shouldCheckLeaks,
+    'merge-leak-depth|m:5' => \$mergeDepth,
+    'new-test-results!' => \$generateNewResults,
+    'no-build' => \$noBuildDumpTool,
+    'nthly=i' => \$testsPerDumpTool,
+    'pixel-tests|p' => \$pixelTests,
+    'platform=s' => \$platform,
+    'port=i' => \$httpdPort,
+    'quiet|q' => \$quiet,
+    'random' => \$randomizeTests,
+    'repeat-each=i' => \$repeatEach,
+    'reset-results' => \$resetResults,
+    'results-directory|o=s' => \$testResultsDirectory,
+    'reverse' => \$reverseTests,
+    'root=s' => \$root,
+    'sample-on-timeout!' => \$runSample,
+    'singly|1' => sub { $testsPerDumpTool = 1; },
+    'skipped=s' => \&validateSkippedArg,
+    'slowest' => \$report10Slowest,
+    'strip-editing-callbacks!' => \$stripEditingCallbacks,
+    'threaded|t' => \$threaded,
+    'timeout=i' => \$timeoutSeconds,
+    'tolerance=f' => \$tolerance,
+    'use-remote-links-to-tests' => \$useRemoteLinksToTests,
+    'valgrind' => \$useValgrind,
+    'verbose|v' => \$verbose,
+    'webkit-test-runner|2' => \$useWebKitTestRunner,
+    # These arguments are ignored (but are used by new-run-webkit-tests).
+    'builder-name=s' => \$builderName,
+    'build-number=s' => \$buildNumber,
+    'master-name=s' => \$masterName,
+    'test-results-server=s' => \$testResultsServer,
+);
+
+if (!$getOptionsResult || $showHelp) {
+    print STDERR $usage;
+    exit 1;
+}
+
+if ($useWebKitTestRunner) {
+    if (isAppleMacWebKit()) {
+        $realPlatform = $platform;
+        $platform = "mac-wk2";
+    } elsif (isAppleWinWebKit()) {
+        $stripEditingCallbacks = 0 unless defined $stripEditingCallbacks;
+        $realPlatform = $platform;
+        $platform = "win-wk2";
+    } elsif (isQt()) {
+        $realPlatform = $platform;
+        $platform = "qt-5.0-wk2";
+    } elsif (isGtk()) {
+        $realPlatform = $platform;
+        $platform = "gtk-wk2";
+    }
+}
+
+$timeoutSeconds *= 10 if $guardMalloc;
+
+$stripEditingCallbacks = isCygwin() unless defined $stripEditingCallbacks;
+
+my $ignoreSkipped = $treatSkipped eq "ignore";
+my $skippedOnly = $treatSkipped eq "only";
+
+my $configuration = configuration();
+
+# We need an environment variable to be able to enable the feature per-slave
+$shouldWaitForHTTPD = $ENV{"WEBKIT_WAIT_FOR_HTTPD"} unless ($shouldWaitForHTTPD);
+$verbose = 1 if $testsPerDumpTool == 1;
+
+if ($shouldCheckLeaks && $testsPerDumpTool > 1000) {
+    print STDERR "\nWARNING: Running more than 1000 tests at a time with MallocStackLogging enabled may cause a crash.\n\n";
+}
+
+# Generating remote links causes a lot of unnecessary spew on GTK build bot
+$useRemoteLinksToTests = 0 if isGtk();
+
+setConfigurationProductDir(Cwd::abs_path($root)) if (defined($root));
+my $productDir = productDir();
+$productDir .= "/bin" if isQt();
+$productDir .= "/Programs" if isGtk();
+
+chdirWebKit();
+
+if (!defined($root) && !$noBuildDumpTool) {
+    # FIXME: We build both DumpRenderTree and WebKitTestRunner for
+    # WebKitTestRunner runs because DumpRenderTree still includes
+    # the DumpRenderTreeSupport module and the TestNetscapePlugin.
+    # These two projects should be factored out into their own
+    # projects.
+    buildDumpTool("DumpRenderTree");
+    buildDumpTool("WebKitTestRunner") if $useWebKitTestRunner;
+}
+
+my $dumpToolName = $useWebKitTestRunner ? "WebKitTestRunner" : "DumpRenderTree";
+
+if (isAppleWinWebKit()) {
+    $dumpToolName .= "_debug" if configurationForVisualStudio() eq "Debug_All";
+    $dumpToolName .= "_debug" if configurationForVisualStudio() eq "Debug_Cairo_CFLite";
+    $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";
+$imageDiffTool .= "_debug" if isCygwin() && configurationForVisualStudio() eq "Debug_All";
+$imageDiffTool .= "_debug" if isCygwin() && configurationForVisualStudio() eq "Debug_Cairo_CFLite";
+die "can't find executable $imageDiffTool (looked in $productDir)\n" if $pixelTests && !-x $imageDiffTool;
+
+checkFrameworks() unless isCygwin();
+
+if (isAppleMacWebKit()) {
+    push @INC, $productDir;
+    require DumpRenderTreeSupport;
+}
+
+my $layoutTestsName = "LayoutTests";
+my $testDirectory = File::Spec->rel2abs($layoutTestsName);
+my $expectedDirectory = $testDirectory;
+my $platformBaseDirectory = catdir($testDirectory, "platform");
+my $platformTestDirectory = catdir($platformBaseDirectory, $platform);
+my @platformResultHierarchy = buildPlatformResultHierarchy();
+my @platformTestHierarchy = buildPlatformTestHierarchy(@platformResultHierarchy);
+
+$expectedDirectory = $ENV{"WebKitExpectedTestResultsDirectory"} if $ENV{"WebKitExpectedTestResultsDirectory"};
+
+$testResultsDirectory = File::Spec->rel2abs($testResultsDirectory);
+# $testResultsDirectory must be empty before testing.
+rmtree $testResultsDirectory;
+my $testResults = File::Spec->catfile($testResultsDirectory, "results.html");
+
+if (isAppleMacWebKit()) {
+    print STDERR "Compiling Java tests\n";
+    my $javaTestsDirectory = catdir($testDirectory, "java");
+
+    if (system("/usr/bin/make", "-C", "$javaTestsDirectory")) {
+        exit 1;
+    }
+} elsif (isCygwin()) {
+    setUpWindowsCrashLogSaving();
+}
+
+print "Running tests from $testDirectory\n";
+if ($pixelTests) {
+    print "Enabling pixel tests with a tolerance of $tolerance%\n";
+    if (isDarwin()) {
+        if (!$useWebKitTestRunner) {
+            print "WARNING: Temporarily changing the main display color profile:\n";
+            print "\tThe colors on your screen will change for the duration of the testing.\n";
+            print "\tThis allows the pixel tests to have consistent color values across all machines.\n";
+        }
+
+        if (isPerianInstalled()) {
+            print "WARNING: Perian's QuickTime component is installed and this may affect pixel test results!\n";
+            print "\tYou should avoid generating new pixel results in this environment.\n";
+            print "\tSee https://bugs.webkit.org/show_bug.cgi?id=22615 for details.\n";
+        }
+    }
+}
+
+system "ln", "-s", $testDirectory, "/tmp/LayoutTests" unless -x "/tmp/LayoutTests";
+
+my %ignoredFiles = ( "results.html" => 1 );
+my %ignoredDirectories = map { $_ => 1 } qw(platform);
+my %ignoredLocalDirectories = map { $_ => 1 } qw(.svn _svn resources script-tests);
+my %supportedFileExtensions = map { $_ => 1 } qw(htm html shtml xml xhtml xhtmlmp pl php mht);
+
+if (!checkWebCoreFeatureSupport("MathML", 0)) {
+    $ignoredDirectories{'mathml'} = 1;
+}
+
+if (!checkWebCoreFeatureSupport("MHTML", 0)) {
+    $ignoredDirectories{'mhtml'} = 1;
+}
+
+# FIXME: We should fix webkitperl/features.pm:hasFeature() to do the correct feature detection for Cygwin.
+if (checkWebCoreFeatureSupport("SVG", 0)) {
+    $supportedFileExtensions{'svg'} = 1;
+} elsif (isCygwin()) {
+    $supportedFileExtensions{'svg'} = 1;
+} else {
+    $ignoredLocalDirectories{'svg'} = 1;
+}
+
+if (!$testHTTP) {
+    $ignoredDirectories{'http'} = 1;
+    $ignoredDirectories{'websocket'} = 1;
+} elsif (!hasHTTPD()) {
+    print "\nNo httpd found. Cannot run http tests.\nPlease use --no-http if you do not want to run http tests.\n";
+    exit 1;
+}
+
+if (!$testWebSocket) {
+    $ignoredDirectories{'websocket'} = 1;
+}
+
+if (!$testMedia) {
+    $ignoredDirectories{'media'} = 1;
+    $ignoredDirectories{'http/tests/media'} = 1;
+}
+
+my $supportedFeaturesResult = "";
+
+if (isCygwin()) {
+    # Collect supported features list
+    my $supportedFeaturesCommand = "\"$dumpTool\" --print-supported-features 2>&1";
+    $supportedFeaturesResult = `$supportedFeaturesCommand 2>&1`;
+}
+
+my $hasAcceleratedCompositing = 0;
+my $has3DRendering = 0;
+
+if (isCygwin()) {
+    $hasAcceleratedCompositing = $supportedFeaturesResult =~ /AcceleratedCompositing/;
+    $has3DRendering = $supportedFeaturesResult =~ /3DRendering/;
+} else {
+    $hasAcceleratedCompositing = checkWebCoreFeatureSupport("Accelerated Compositing", 0);
+    $has3DRendering = checkWebCoreFeatureSupport("3D Rendering", 0);
+}
+
+if (!$hasAcceleratedCompositing) {
+    $ignoredDirectories{'compositing'} = 1;
+
+    # This test has slightly different floating-point rounding when accelerated
+    # compositing is enabled.
+    $ignoredFiles{'svg/custom/use-on-symbol-inside-pattern.svg'} = 1;
+
+    # This test has an iframe that is put in a layer only in compositing mode.
+    $ignoredFiles{'media/media-document-audio-repaint.html'} = 1;
+
+    if (isAppleWebKit()) {
+        # In Apple's ports, the default controls for <video> contain a "full
+        # screen" button only if accelerated compositing is enabled.
+        $ignoredFiles{'media/controls-after-reload.html'} = 1;
+        $ignoredFiles{'media/controls-drag-timebar.html'} = 1;
+        $ignoredFiles{'media/controls-strict.html'} = 1;
+        $ignoredFiles{'media/controls-styling.html'} = 1;
+        $ignoredFiles{'media/controls-without-preload.html'} = 1;
+        $ignoredFiles{'media/video-controls-rendering.html'} = 1;
+        $ignoredFiles{'media/video-display-toggle.html'} = 1;
+        $ignoredFiles{'media/video-no-audio.html'} = 1;
+    }
+
+    # Here we're using !$hasAcceleratedCompositing as a proxy for "is a headless XP machine" (like
+    # our test slaves). Headless XP machines can neither support accelerated compositing nor pass
+    # this test, so skipping the test here is expedient, if a little sloppy. See
+    # <http://webkit.org/b/48333>.
+    $ignoredFiles{'platform/win/plugins/npn-invalidate-rect-invalidates-window.html'} = 1 if isAppleWinWebKit();
+}
+
+if (!$has3DRendering) {
+    $ignoredDirectories{'animations/3d'} = 1;
+    $ignoredDirectories{'transforms/3d'} = 1;
+
+    # These tests use the -webkit-transform-3d media query.
+    $ignoredFiles{'fast/media/mq-transform-02.html'} = 1;
+    $ignoredFiles{'fast/media/mq-transform-03.html'} = 1;
+}
+
+if (!checkWebCoreFeatureSupport("3D Canvas", 0)) {
+    $ignoredDirectories{'fast/canvas/webgl'} = 1;
+    $ignoredDirectories{'compositing/webgl'} = 1;
+    $ignoredDirectories{'http/tests/canvas/webgl'} = 1;
+}
+
+if (isAppleMacWebKit() && $platform ne "mac-wk2" && osXVersion()->{minor} >= 6 && architecture() =~ /x86_64/) {
+    # This test relies on executing JavaScript during NPP_Destroy, which isn't supported with
+    # out-of-process plugins in WebKit1. See <http://webkit.org/b/58077>.
+    $ignoredFiles{'plugins/npp-set-window-called-during-destruction.html'} = 1;
+}
+
+processIgnoreTests(join(',', @ignoreTests), "ignore-tests") if @ignoreTests;
+if (!$ignoreSkipped) {
+    if (!$skippedOnly || @ARGV == 0) {
+        readSkippedFiles("");
+    } else {
+        # Since readSkippedFiles() appends to @ARGV, we must use a foreach
+        # loop so that we only iterate over the original argument list.
+        foreach my $argnum (0 .. $#ARGV) {
+            readSkippedFiles(shift @ARGV);
+        }
+    }
+}
+
+my @tests = findTestsToRun();
+
+die "no tests to run\n" if !@tests;
+
+my %counts;
+my %tests;
+my %imagesPresent;
+my %imageDifferences;
+my %durations;
+my $count = 0;
+my $leaksOutputFileNumber = 1;
+my $totalLeaks = 0;
+my $stoppedRunningEarlyMessage;
+
+my @toolArgs = ();
+push @toolArgs, "--pixel-tests" if $pixelTests;
+push @toolArgs, "--threaded" if $threaded;
+push @toolArgs, "--complex-text" if $complexText;
+push @toolArgs, "--gc-between-tests" if $gcBetweenTests;
+push @toolArgs, "-";
+
+my @diffToolArgs = ();
+push @diffToolArgs, "--tolerance", $tolerance;
+
+$| = 1;
+
+my $dumpToolPID;
+my $isDumpToolOpen = 0;
+my $dumpToolCrashed = 0;
+my $imageDiffToolPID;
+my $isDiffToolOpen = 0;
+
+my $atLineStart = 1;
+my $lastDirectory = "";
+
+my $isHttpdOpen = 0;
+my $isWebSocketServerOpen = 0;
+my $webSocketServerPidFile = 0;
+my $failedToStartWebSocketServer = 0;
+# wss is disabled until all platforms support pyOpenSSL.
+# my $webSocketSecureServerPID = 0;
+
+sub catch_pipe { $dumpToolCrashed = 1; }
+$SIG{"PIPE"} = "catch_pipe";
+
+print "Testing ", scalar @tests, " test cases";
+print " $iterations times" if ($iterations > 1);
+print ", repeating each test $repeatEach times" if ($repeatEach > 1);
+print ".\n";
+
+my $overallStartTime = time;
+
+my %expectedResultPaths;
+
+my @originalTests = @tests;
+# Add individual test repetitions
+if ($repeatEach > 1) {
+    @tests = ();
+    foreach my $test (@originalTests) {
+        for (my $i = 0; $i < $repeatEach; $i++) {
+            push(@tests, $test);
+        }
+    }
+}
+# Add test set repetitions
+for (my $i = 1; $i < $iterations; $i++) {
+    push(@tests, @originalTests);
+}
+
+my $absTestResultsDirectory = resolveAndMakeTestResultsDirectory();
+open my $tests_run_fh, '>', "$absTestResultsDirectory/tests_run.txt" or die $!;
+
+for my $test (@tests) {
+    my $newDumpTool = not $isDumpToolOpen;
+    openDumpTool();
+
+    my $base = stripExtension($test);
+    my $expectedExtension = ".txt";
+    
+    my $dir = $base;
+    $dir =~ s|/[^/]+$||;
+
+    if ($newDumpTool || $dir ne $lastDirectory) {
+        foreach my $logue (epiloguesAndPrologues($newDumpTool ? "" : $lastDirectory, $dir)) {
+            if (isCygwin()) {
+                $logue = toWindowsPath($logue);
+            } else {
+                $logue = canonpath($logue);
+            }
+            if ($verbose) {
+                print "running epilogue or prologue $logue\n";
+            }
+            print OUT "$logue\n";
+            # Throw away output from DumpRenderTree.
+            # Once for the test output and once for pixel results (empty)
+            while (<IN>) {
+                last if /#EOF/;
+            }
+            while (<IN>) {
+                last if /#EOF/;
+            }
+        }
+    }
+
+    if ($verbose) {
+        print "running $test -> ";
+        $atLineStart = 0;
+    } elsif (!$quiet) {
+        if ($dir ne $lastDirectory) {
+            print "\n" unless $atLineStart;
+            print "$dir ";
+        }
+        print ".";
+        $atLineStart = 0;
+    }
+
+    $lastDirectory = $dir;
+
+    my $result;
+
+    my $startTime = time if $report10Slowest;
+
+    print $tests_run_fh "$test\n";
+
+    # Try to read expected hash file for pixel tests
+    my $suffixPixelTest = "";
+    if ($pixelTests) {
+        # ' is the separator between arguments.
+        $suffixPixelTest = "'--pixel-test";
+        if (!$resetResults) {
+            my $expectedPixelDir = expectedDirectoryForTest($base, 0, "png");
+            if (my $expectedHash = readChecksumFromPng(File::Spec->catfile($expectedPixelDir, "$base-$expectedTag.png"))) {
+                # Format expected hash into a suffix string that is appended to the path / URL passed to DRT.
+                $suffixPixelTest = "'--pixel-test'$expectedHash";
+            }
+        }
+    }
+
+    if ($test =~ /^http\//) {
+        configureAndOpenHTTPDIfNeeded();
+        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\//) {
+            my $path = canonpath($test);
+            $path =~ s/^http\/tests\///;
+            print OUT "http://127.0.0.1:$httpdPort/$path$suffixPixelTest\n";
+        } elsif ($test =~ /^http\/tests\/ssl\//) {
+            my $path = canonpath($test);
+            $path =~ s/^http\/tests\///;
+            print OUT "https://127.0.0.1:$httpdSSLPort/$path$suffixPixelTest\n";
+        } else {
+            my $testPath = "$testDirectory/$test";
+            if (isCygwin()) {
+                $testPath = toWindowsPath($testPath);
+            } else {
+                $testPath = canonpath($testPath);
+            }
+            print OUT "$testPath$suffixPixelTest\n";
+        }
+    } else {
+        my $testPath = "$testDirectory/$test";
+        if (isCygwin()) {
+            $testPath = toWindowsPath($testPath);
+        } else {
+            $testPath = canonpath($testPath);
+        }
+        print OUT "$testPath$suffixPixelTest\n" if defined $testPath;
+    }
+
+    # DumpRenderTree is expected to dump two "blocks" to stdout for each test.
+    # Each block is terminated by a #EOF on a line by itself.
+    # The first block is the output of the test (in text, RenderTree or other formats).
+    # The second block is for optional pixel data in PNG format, and may be empty if
+    # pixel tests are not being run, or the test does not dump pixels (e.g. text tests).
+    my $readResults = readFromDumpToolWithTimer(IN, ERROR);
+
+    my $actual = $readResults->{output};
+    my $error = $readResults->{error};
+
+    $expectedExtension = $readResults->{extension};
+    my $expectedFileName = "$base-$expectedTag.$expectedExtension";
+
+    my $isText = isTextOnlyTest($actual);
+
+    my $expectedDir = expectedDirectoryForTest($base, $isText, $expectedExtension);
+    $expectedResultPaths{$base} = File::Spec->catfile($expectedDir, $expectedFileName);
+
+    unless ($readResults->{status} eq "success") {
+        my $crashed = $readResults->{status} eq "crashed";
+        my $webProcessCrashed = $readResults->{status} eq "webProcessCrashed";
+        testCrashedOrTimedOut($test, $base, $crashed, $webProcessCrashed, $actual, $error);
+        countFinishedTest($test, $base, $webProcessCrashed ? "webProcessCrash" : $crashed ? "crash" : "timedout", 0);
+        last if stopRunningTestsEarlyIfNeeded();
+        next;
+    }
+
+    $durations{$test} = time - $startTime if $report10Slowest;
+
+    my $expected;
+
+    if (!$resetResults && open EXPECTED, "<", $expectedResultPaths{$base}) {
+        $expected = "";
+        while (<EXPECTED>) {
+            next if $stripEditingCallbacks && $_ =~ /^EDITING DELEGATE:/;
+            $expected .= $_;
+        }
+        close EXPECTED;
+    }
+
+    if ($ignoreMetrics && !$isText && defined $expected) {
+        ($actual, $expected) = stripMetrics($actual, $expected);
+    }
+
+    if ($shouldCheckLeaks && $testsPerDumpTool == 1) {
+        print "        $test -> ";
+    }
+
+    my $actualPNG = "";
+    my $diffPNG = "";
+    my $diffPercentage = 0;
+    my $diffResult = "passed";
+
+    my $actualHash = "";
+    my $expectedHash = "";
+    my $actualPNGSize = 0;
+
+    while (<IN>) {
+        last if /#EOF/;
+        if (/ActualHash: ([a-f0-9]{32})/) {
+            $actualHash = $1;
+        } elsif (/ExpectedHash: ([a-f0-9]{32})/) {
+            $expectedHash = $1;
+        } elsif (/Content-Length: (\d+)\s*/) {
+            $actualPNGSize = $1;
+            read(IN, $actualPNG, $actualPNGSize);
+        }
+    }
+
+    if ($verbose && $pixelTests && !$resetResults && $actualPNGSize) {
+        if ($actualHash eq "" && $expectedHash eq "") {
+            printFailureMessageForTest($test, "WARNING: actual & expected pixel hashes are missing!");
+        } elsif ($actualHash eq "") {
+            printFailureMessageForTest($test, "WARNING: actual pixel hash is missing!");
+        } elsif ($expectedHash eq "") {
+            printFailureMessageForTest($test, "WARNING: expected pixel hash is missing!");
+        }
+    }
+
+    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 $expectedPNGPath) {
+                my $expectedPNGSize = -s $expectedPNGPath;
+                my $expectedPNG = "";
+                open EXPECTEDPNG, $expectedPNGPath;
+                read(EXPECTEDPNG, $expectedPNG, $expectedPNGSize);
+
+                openDiffTool();
+                print DIFFOUT "Content-Length: $actualPNGSize\n";
+                print DIFFOUT $actualPNG;
+
+                print DIFFOUT "Content-Length: $expectedPNGSize\n";
+                print DIFFOUT $expectedPNG;
+
+                while (<DIFFIN>) {
+                    last if /^error/ || /^diff:/;
+                    if (/Content-Length: (\d+)\s*/) {
+                        read(DIFFIN, $diffPNG, $1);
+                    }
+                }
+
+                if (/^diff: (.+)% (passed|failed)/) {
+                    $diffPercentage = $1 + 0;
+                    $imageDifferences{$base} = $diffPercentage;
+                    $diffResult = $2;
+                }
+                
+                if (!$diffPercentage) {
+                    printFailureMessageForTest($test, "pixel hash failed (but pixel test still passes)");
+                }
+            } elsif ($verbose) {
+                printFailureMessageForTest($test, "WARNING: expected image is missing!");
+            }
+        }
+
+        if ($resetResults || !-f $expectedPNGPath) {
+            if (!$addPlatformExceptions) {
+                mkpath catfile($expectedPixelDir, dirname($base)) if $testDirectory ne $expectedPixelDir;
+                writeToFile($expectedPNGPath, $actualPNG);
+            } else {
+                mkpath catfile($platformTestDirectory, dirname($base));
+                writeToFile("$platformTestDirectory/$base-$expectedTag.png", $actualPNG);
+            }
+        }
+    }
+
+    if (dumpToolDidCrash()) {
+        $result = "crash";
+        testCrashedOrTimedOut($test, $base, 1, 0, $actual, $error);
+    } elsif (!defined $expected) {
+        if ($verbose) {
+            print "new " . ($resetResults ? "result" : "test");
+        }
+        $result = "new";
+
+        if ($generateNewResults || $resetResults) {
+            if (!$addPlatformExceptions) {
+                mkpath catfile($expectedDir, dirname($base)) if $testDirectory ne $expectedDir;
+                writeToFile("$expectedDir/$expectedFileName", $actual);
+            } else {
+                mkpath catfile($platformTestDirectory, dirname($base));
+                writeToFile("$platformTestDirectory/$expectedFileName", $actual);
+            }
+        }
+        deleteExpectedAndActualResults($base);
+        recordActualResultsAndDiff($base, $actual);
+        if (!$resetResults) {
+            # Always print the file name for new tests, as they will probably need some manual inspection.
+            # in verbose mode we already printed the test case, so no need to do it again.
+            unless ($verbose) {
+                print "\n" unless $atLineStart;
+                print "$test -> ";
+            }
+            my $resultsDir = catdir($expectedDir, dirname($base));
+            if (!$verbose) {
+                print "new";
+            }
+            if ($generateNewResults) {
+                print " (results generated in $resultsDir)";
+            }
+            print "\n" unless $atLineStart;
+            $atLineStart = 1;
+        }
+    } elsif ($actual eq $expected && $diffResult eq "passed") {
+        if ($verbose) {
+            print "succeeded\n";
+            $atLineStart = 1;
+        }
+        $result = "match";
+        deleteExpectedAndActualResults($base);
+    } else {
+        $result = "mismatch";
+
+        my $pixelTestFailed = $pixelTests && $diffPNG && $diffPNG ne "";
+        my $testFailed = $actual ne $expected;
+
+        my $message = !$testFailed ? "pixel test failed" : "failed";
+
+        if (($testFailed || $pixelTestFailed) && $addPlatformExceptions) {
+            my $testBase = catfile($testDirectory, $base);
+            my $expectedBase = catfile($expectedDir, $base);
+            my $testIsMaximallyPlatformSpecific = $testBase =~ m|^\Q$platformTestDirectory\E/|;
+            my $expectedResultIsMaximallyPlatformSpecific = $expectedBase =~ m|^\Q$platformTestDirectory\E/|;
+            if (!$testIsMaximallyPlatformSpecific && !$expectedResultIsMaximallyPlatformSpecific) {
+                mkpath catfile($platformTestDirectory, dirname($base));
+                if ($testFailed) {
+                    my $expectedFile = catfile($platformTestDirectory, "$expectedFileName");
+                    writeToFile("$expectedFile", $actual);
+                }
+                if ($pixelTestFailed) {
+                    my $expectedFile = catfile($platformTestDirectory, "$base-$expectedTag.png");
+                    writeToFile("$expectedFile", $actualPNG);
+                }
+                $message .= " (results generated in $platformTestDirectory)";
+            }
+        }
+
+        printFailureMessageForTest($test, $message);
+
+        my $dir = "$testResultsDirectory/$base";
+        $dir =~ s|/([^/]+)$|| or die "Failed to find test name from base\n";
+        my $testName = $1;
+        mkpath $dir;
+
+        deleteExpectedAndActualResults($base);
+        recordActualResultsAndDiff($base, $actual);
+
+        if ($pixelTestFailed) {
+            $imagesPresent{$base} = 1;
+
+            writeToFile("$testResultsDirectory/$base-$actualTag.png", $actualPNG);
+            writeToFile("$testResultsDirectory/$base-$diffsTag.png", $diffPNG);
+
+            my $expectedPixelDir = expectedDirectoryForTest($base, 0, "png");
+            copy("$expectedPixelDir/$base-$expectedTag.png", "$testResultsDirectory/$base-$expectedTag.png");
+
+            open DIFFHTML, ">$testResultsDirectory/$base-$diffsTag.html" or die;
+            print DIFFHTML "<html>\n";
+            print DIFFHTML "<head>\n";
+            print DIFFHTML "<title>$base Image Compare</title>\n";
+            print DIFFHTML "<script language=\"Javascript\" type=\"text/javascript\">\n";
+            print DIFFHTML "var currentImage = 0;\n";
+            print DIFFHTML "var imageNames = new Array(\"Actual\", \"Expected\");\n";
+            print DIFFHTML "var imagePaths = new Array(\"$testName-$actualTag.png\", \"$testName-$expectedTag.png\");\n";
+            if (-f "$testDirectory/$base-w3c.png") {
+                copy("$testDirectory/$base-w3c.png", "$testResultsDirectory/$base-w3c.png");
+                print DIFFHTML "imageNames.push(\"W3C\");\n";
+                print DIFFHTML "imagePaths.push(\"$testName-w3c.png\");\n";
+            }
+            print DIFFHTML "function animateImage() {\n";
+            print DIFFHTML "    var image = document.getElementById(\"animatedImage\");\n";
+            print DIFFHTML "    var imageText = document.getElementById(\"imageText\");\n";
+            print DIFFHTML "    image.src = imagePaths[currentImage];\n";
+            print DIFFHTML "    imageText.innerHTML = imageNames[currentImage] + \" Image\";\n";
+            print DIFFHTML "    currentImage = (currentImage + 1) % imageNames.length;\n";
+            print DIFFHTML "    setTimeout('animateImage()',2000);\n";
+            print DIFFHTML "}\n";
+            print DIFFHTML "</script>\n";
+            print DIFFHTML "</head>\n";
+            print DIFFHTML "<body onLoad=\"animateImage();\">\n";
+            print DIFFHTML "<table>\n";
+            if ($diffPercentage) {
+                print DIFFHTML "<tr>\n";
+                print DIFFHTML "<td>Difference between images: <a href=\"$testName-$diffsTag.png\">$diffPercentage%</a></td>\n";
+                print DIFFHTML "</tr>\n";
+            }
+            print DIFFHTML "<tr>\n";
+            print DIFFHTML "<td><a href=\"" . toURL("$testDirectory/$test") . "\">test file</a></td>\n";
+            print DIFFHTML "</tr>\n";
+            print DIFFHTML "<tr>\n";
+            print DIFFHTML "<td id=\"imageText\" style=\"text-weight: bold;\">Actual Image</td>\n";
+            print DIFFHTML "</tr>\n";
+            print DIFFHTML "<tr>\n";
+            print DIFFHTML "<td><img src=\"$testName-$actualTag.png\" id=\"animatedImage\"></td>\n";
+            print DIFFHTML "</tr>\n";
+            print DIFFHTML "</table>\n";
+            print DIFFHTML "</body>\n";
+            print DIFFHTML "</html>\n";
+        }
+    }
+
+    if ($error) {
+        my $dir = dirname(File::Spec->catdir($testResultsDirectory, $base));
+        mkpath $dir;
+        
+        writeToFile(File::Spec->catfile($testResultsDirectory, "$base-$errorTag.txt"), $error);
+        
+        $counts{error}++;
+        push @{$tests{error}}, $test;
+    }
+
+    countFinishedTest($test, $base, $result, $isText);
+    last if stopRunningTestsEarlyIfNeeded();
+}
+
+close($tests_run_fh);
+
+my $totalTestingTime = time - $overallStartTime;
+my $waitTime = getWaitTime();
+if ($waitTime > 0.1) {
+    my $normalizedTestingTime = $totalTestingTime - $waitTime;
+    printf "\n%0.2fs HTTPD waiting time\n", $waitTime . "";
+    printf "%0.2fs normalized testing time", $normalizedTestingTime . "";
+}
+printf "\n%0.2fs total testing time\n", $totalTestingTime . "";
+
+!$isDumpToolOpen || die "Failed to close $dumpToolName.\n";
+
+$isHttpdOpen = !closeHTTPD();
+closeWebSocketServer();
+
+# Because multiple instances of this script are running concurrently we cannot 
+# safely delete this symlink.
+# system "rm /tmp/LayoutTests";
+
+# FIXME: Do we really want to check the image-comparison tool for leaks every time?
+if ($isDiffToolOpen && $shouldCheckLeaks) {
+    $totalLeaks += countAndPrintLeaks("ImageDiff", $imageDiffToolPID, "$testResultsDirectory/ImageDiff-leaks.txt");
+}
+
+if ($totalLeaks) {
+    if ($mergeDepth) {
+        parseLeaksandPrintUniqueLeaks();
+    } else { 
+        print "\nWARNING: $totalLeaks total leaks found!\n";
+        print "See above for individual leaks results.\n" if ($leaksOutputFileNumber > 2);
+    }
+}
+
+close IN;
+close OUT;
+close ERROR;
+
+if ($report10Slowest) {
+    print "\n\nThe 10 slowest tests:\n\n";
+    my $count = 0;
+    for my $test (sort slowestcmp keys %durations) {
+        printf "%0.2f secs: %s\n", $durations{$test}, $test;
+        last if ++$count == 10;
+    }
+}
+
+print "\n";
+
+if ($skippedOnly && $counts{"match"}) {
+    print "The following tests are in the Skipped file (" . File::Spec->abs2rel("$platformTestDirectory/Skipped", $testDirectory) . "), but succeeded:\n";
+    foreach my $test (@{$tests{"match"}}) {
+        print "  $test\n";
+    }
+}
+
+if ($resetResults || ($counts{match} && $counts{match} == $count)) {
+    print "all $count test cases succeeded\n";
+    unlink $testResults;
+    exit;
+}
+
+printResults();
+
+mkpath $testResultsDirectory;
+
+open HTML, ">", $testResults or die "Failed to open $testResults. $!";
+print HTML <<EOF;
+<html>
+<head>
+<title>Layout Test Results</title>
+<style>
+.stopped-running-early-message {
+    border: 3px solid #d00;
+    font-weight: bold;
+}
+</style>
+</head>
+</body>
+EOF
+
+if ($ignoreMetrics) {
+    print HTML "<h4>Tested with metrics ignored.</h4>";
+}
+
+if ($stoppedRunningEarlyMessage) {
+    print HTML "<p class='stopped-running-early-message'>$stoppedRunningEarlyMessage</p>";
+}
+
+print HTML htmlForResultsSection(@{$tests{mismatch}}, "Tests where results did not match expected results", \&linksForMismatchTest);
+print HTML htmlForResultsSection(@{$tests{timedout}}, "Tests that timed out", \&linksForErrorTest);
+print HTML htmlForResultsSection(@{$tests{crash}}, "Tests that caused the DumpRenderTree tool to crash", \&linksForErrorTest);
+print HTML htmlForResultsSection(@{$tests{webProcessCrash}}, "Tests that caused the Web process to crash", \&linksForErrorTest);
+print HTML htmlForResultsSection(@{$tests{error}}, "Tests that had stderr output", \&linksForErrorTest);
+print HTML htmlForResultsSection(@{$tests{new}}, "Tests that had no expected results (probably new)", \&linksForNewTest);
+
+print HTML "<p>httpd access log: <a href=\"access_log.txt\">access_log.txt</a></p>\n";
+print HTML "<p>httpd error log: <a href=\"error_log.txt\">error_log.txt</a></p>\n";
+
+print HTML "</body>\n";
+print HTML "</html>\n";
+close HTML;
+
+my @configurationArgs = argumentsForConfiguration();
+
+if (isGtk()) {
+  push(@configurationArgs, '-2') if $useWebKitTestRunner;
+  system "Tools/Scripts/run-launcher", @configurationArgs, "file://".$testResults if $launchSafari;
+} elsif (isQt()) {
+  if (getQtVersion() lt "5.0") {
+    unshift @configurationArgs, qw(-style windows);
+    unshift @configurationArgs, qw(-graphicssystem raster);
+  }
+  if (isCygwin()) {
+    $testResults = "/" . toWindowsPath($testResults);
+    $testResults =~ s/\\/\//g;
+  }
+  push(@configurationArgs, '-2') if $useWebKitTestRunner;
+  system "Tools/Scripts/run-launcher", @configurationArgs, "file://".$testResults if $launchSafari;
+} elsif (isCygwin()) {
+  system "cygstart", $testResults if $launchSafari;
+} elsif (isWindows()) {
+  system "start", $testResults if $launchSafari;
+} else {
+  system "Tools/Scripts/run-safari", @configurationArgs, "-NSOpen", $testResults if $launchSafari;
+}
+
+closeCygpaths() if isCygwin();
+
+exit 1;
+
+sub countAndPrintLeaks($$$)
+{
+    my ($dumpToolName, $dumpToolPID, $leaksFilePath) = @_;
+
+    print "\n" unless $atLineStart;
+    $atLineStart = 1;
+
+    # We are excluding the following reported leaks so they don't get in our way when looking for WebKit leaks:
+    # This allows us ignore known leaks and only be alerted when new leaks occur. Some leaks are in the old
+    # versions of the system frameworks that are being used by the leaks bots. Even though a leak has been
+    # fixed, it will be listed here until the bot has been updated with the newer frameworks.
+
+    my @typesToExclude = (
+    );
+
+    my @callStacksToExclude = (
+        "Flash_EnforceLocalSecurity", # leaks in Flash plug-in code, rdar://problem/4449747
+        "ScanFromString", # <http://code.google.com/p/angleproject/issues/detail?id=249> leak in ANGLE
+    );
+
+    if (isSnowLeopard()) {
+        push @callStacksToExclude, (
+            "readMakerNoteProps", # <rdar://problem/7156432> leak in ImageIO
+            "QTKitMovieControllerView completeUISetup", # <rdar://problem/7155156> leak in QTKit
+            "getVMInitArgs", # <rdar://problem/7714444> leak in Java
+            "Java_java_lang_System_initProperties", # <rdar://problem/7714465> leak in Java
+            "glrCompExecuteKernel", # <rdar://problem/7815391> leak in graphics driver while using OpenGL
+            "NSNumberFormatter getObjectValue:forString:errorDescription:", # <rdar://problem/7149350> Leak in NSNumberFormatter
+        );
+    }
+
+    if (isLion()) {
+        push @callStacksToExclude, (
+            "FigByteFlumeCustomURLCreateWithURL", # <rdar://problem/10461926> leak in CoreMedia
+            "PDFPage\\(PDFPageInternal\\) pageLayoutIfAvail", # <rdar://problem/10462055> leak in PDFKit
+            "SecTransformExecute", # <rdar://problem/10470667> leak in Security.framework
+            "_NSCopyStyleRefForFocusRingStyleClip", # <rdar://problem/10462031> leak in AppKit
+        );
+    }
+
+    my $leaksTool = sourceDir() . "/Tools/Scripts/run-leaks";
+    my $excludeString = "--exclude-callstack '" . (join "' --exclude-callstack '", @callStacksToExclude) . "'";
+    $excludeString .= " --exclude-type '" . (join "' --exclude-type '", @typesToExclude) . "'" if @typesToExclude;
+
+    print " ? checking for leaks in $dumpToolName\n";
+    my $leaksOutput = `$leaksTool $excludeString $dumpToolPID`;
+    my ($count, $bytes) = $leaksOutput =~ /Process $dumpToolPID: (\d+) leaks? for (\d+) total/;
+    my ($excluded) = $leaksOutput =~ /(\d+) leaks? excluded/;
+
+    my $adjustedCount = $count;
+    $adjustedCount -= $excluded if $excluded;
+
+    if (!$adjustedCount) {
+        print " - no leaks found\n";
+        unlink $leaksFilePath;
+        return 0;
+    } else {
+        my $dir = $leaksFilePath;
+        $dir =~ s|/[^/]+$|| or die;
+        mkpath $dir;
+
+        if ($excluded) {
+            print " + $adjustedCount leaks ($bytes bytes including $excluded excluded leaks) were found, details in $leaksFilePath\n";
+        } else {
+            print " + $count leaks ($bytes bytes) were found, details in $leaksFilePath\n";
+        }
+
+        writeToFile($leaksFilePath, $leaksOutput);
+        
+        push @leaksFilenames, $leaksFilePath;
+    }
+
+    return $adjustedCount;
+}
+
+sub writeToFile($$)
+{
+    my ($filePath, $contents) = @_;
+    open NEWFILE, ">", "$filePath" or die "Could not create $filePath. $!\n";
+    print NEWFILE $contents;
+    close NEWFILE;
+}
+
+# Break up a path into the directory (with slash) and base name.
+sub splitpath($)
+{
+    my ($path) = @_;
+
+    my $pathSeparator = "/";
+    my $dirname = dirname($path) . $pathSeparator;
+    $dirname = "" if $dirname eq "." . $pathSeparator;
+
+    return ($dirname, basename($path));
+}
+
+# Sort first by directory, then by file, so all paths in one directory are grouped
+# rather than being interspersed with items from subdirectories.
+# Use numericcmp to sort directory and filenames to make order logical.
+sub pathcmp($$)
+{
+    my ($patha, $pathb) = @_;
+
+    my ($dira, $namea) = splitpath($patha);
+    my ($dirb, $nameb) = splitpath($pathb);
+
+    return numericcmp($dira, $dirb) if $dira ne $dirb;
+    return numericcmp($namea, $nameb);
+}
+
+# Sort numeric parts of strings as numbers, other parts as strings.
+# Makes 1.33 come after 1.3, which is cool.
+sub numericcmp($$)
+{
+    my ($aa, $bb) = @_;
+
+    my @a = split /(\d+)/, $aa;
+    my @b = split /(\d+)/, $bb;
+
+    # Compare one chunk at a time.
+    # Each chunk is either all numeric digits, or all not numeric digits.
+    while (@a && @b) {
+        my $a = shift @a;
+        my $b = shift @b;
+        
+        # Use numeric comparison if chunks are non-equal numbers.
+        return $a <=> $b if $a =~ /^\d/ && $b =~ /^\d/ && $a != $b;
+
+        # Use string comparison if chunks are any other kind of non-equal string.
+        return $a cmp $b if $a ne $b;
+    }
+    
+    # One of the two is now empty; compare lengths for result in this case.
+    return @a <=> @b;
+}
+
+# Sort slowest tests first.
+sub slowestcmp($$)
+{
+    my ($testa, $testb) = @_;
+
+    my $dura = $durations{$testa};
+    my $durb = $durations{$testb};
+    return $durb <=> $dura if $dura != $durb;
+    return pathcmp($testa, $testb);
+}
+
+sub launchWithEnv(\@\%)
+{
+    my ($args, $env) = @_;
+
+    # Dump the current environment as perl code and then put it in quotes so it is one parameter.
+    my $environmentDumper = Data::Dumper->new([\%{$env}], [qw(*ENV)]);
+    $environmentDumper->Indent(0);
+    $environmentDumper->Purity(1);
+    my $allEnvVars = $environmentDumper->Dump();
+    unshift @{$args}, "\"$allEnvVars\"";
+
+    my $execScript = File::Spec->catfile(sourceDir(), qw(Tools Scripts execAppWithEnv));
+    unshift @{$args}, $perlInterpreter, $execScript;
+    return @{$args};
+}
+
+sub resolveAndMakeTestResultsDirectory()
+{
+    my $absTestResultsDirectory = File::Spec->rel2abs(glob $testResultsDirectory);
+    mkpath $absTestResultsDirectory;
+    return $absTestResultsDirectory;
+}
+
+sub openDiffTool()
+{
+    return if $isDiffToolOpen;
+    return if !$pixelTests;
+
+    my %CLEAN_ENV;
+    $CLEAN_ENV{MallocStackLogging} = 1 if $shouldCheckLeaks;
+    $imageDiffToolPID = open2(\*DIFFIN, \*DIFFOUT, $imageDiffTool, launchWithEnv(@diffToolArgs, %CLEAN_ENV)) or die "unable to open $imageDiffTool\n";
+    $isDiffToolOpen = 1;
+}
+
+sub buildDumpTool($)
+{
+    my ($dumpToolName) = @_;
+
+    my $dumpToolBuildScript =  "build-" . lc($dumpToolName);
+    print STDERR "Running $dumpToolBuildScript\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, $perlInterpreter, File::Spec->catfile(qw(Tools Scripts), $dumpToolBuildScript), @args) or die "Failed to run build-dumprendertree";
+    close($childIn);
+    waitpid $buildProcess, 0;
+    my $buildResult = $?;
+    close($childOut);
+    close($childErr);
+
+    close DEVNULL if ($quiet);
+
+    if ($buildResult) {
+        print STDERR "Compiling $dumpToolName failed!\n";
+        exit exitStatus($buildResult);
+    }
+}
+
+sub openDumpTool()
+{
+    return if $isDumpToolOpen;
+
+    if ($verbose && $testsPerDumpTool != 1) {
+        print "| Opening DumpTool |\n";
+    }
+
+    my %CLEAN_ENV;
+
+    # Generic environment variables
+    if (defined $ENV{'WEBKIT_TESTFONTS'}) {
+        $CLEAN_ENV{WEBKIT_TESTFONTS} = $ENV{'WEBKIT_TESTFONTS'};
+    }
+
+    # unique temporary directory for each DumpRendertree - needed for running more DumpRenderTree in parallel
+    $CLEAN_ENV{DUMPRENDERTREE_TEMP} = File::Temp::tempdir('DumpRenderTree-XXXXXX', TMPDIR => 1, CLEANUP => 1);
+    $CLEAN_ENV{XML_CATALOG_FILES} = ""; # work around missing /etc/catalog <rdar://problem/4292995>
+    $CLEAN_ENV{LOCAL_RESOURCE_ROOT} = $testDirectory; # Used by layoutTestConstroller.pathToLocalResource()
+
+    # Platform spesifics
+    if (isLinux()) {
+        if (defined $ENV{'DISPLAY'}) {
+            $CLEAN_ENV{DISPLAY} = $ENV{'DISPLAY'};
+        } else {
+            $CLEAN_ENV{DISPLAY} = ":1";
+        }
+        if (defined $ENV{'XAUTHORITY'}) {
+            $CLEAN_ENV{XAUTHORITY} = $ENV{'XAUTHORITY'};
+        }
+
+        $CLEAN_ENV{HOME} = $ENV{'HOME'};
+        $CLEAN_ENV{LANG} = $ENV{'LANG'};
+
+        if (defined $ENV{'LD_LIBRARY_PATH'}) {
+            $CLEAN_ENV{LD_LIBRARY_PATH} = $ENV{'LD_LIBRARY_PATH'};
+        }
+        if (defined $ENV{'DBUS_SESSION_BUS_ADDRESS'}) {
+            $CLEAN_ENV{DBUS_SESSION_BUS_ADDRESS} = $ENV{'DBUS_SESSION_BUS_ADDRESS'};
+        }
+    } elsif (isDarwin()) {
+        if (defined $ENV{'DYLD_LIBRARY_PATH'}) {
+            $CLEAN_ENV{DYLD_LIBRARY_PATH} = $ENV{'DYLD_LIBRARY_PATH'};
+        }
+        if (defined $ENV{'HOME'}) {
+            $CLEAN_ENV{HOME} = $ENV{'HOME'};
+        }
+
+        $CLEAN_ENV{DYLD_FRAMEWORK_PATH} = $productDir;
+        $CLEAN_ENV{DYLD_INSERT_LIBRARIES} = "/usr/lib/libgmalloc.dylib" if $guardMalloc;
+    } elsif (isCygwin()) {
+        $CLEAN_ENV{HOMEDRIVE} = $ENV{'HOMEDRIVE'};
+        $CLEAN_ENV{HOMEPATH} = $ENV{'HOMEPATH'};
+        $CLEAN_ENV{_NT_SYMBOL_PATH} = $ENV{_NT_SYMBOL_PATH};
+    }
+
+    # Port specifics
+    if (isGtk()) {
+        $CLEAN_ENV{LIBOVERLAY_SCROLLBAR} = "0";
+        $CLEAN_ENV{GTK_MODULES} = "gail";
+        $CLEAN_ENV{WEBKIT_INSPECTOR_PATH} = "$productDir/resources/inspector";
+
+        if ($useWebKitTestRunner) {
+            my $injectedBundlePath = productDir() . "/Libraries/.libs/libTestRunnerInjectedBundle";
+            $CLEAN_ENV{TEST_RUNNER_INJECTED_BUNDLE_FILENAME} = $injectedBundlePath;
+            my $testPluginPath = productDir() . "/TestNetscapePlugin/.libs";
+            $CLEAN_ENV{TEST_RUNNER_TEST_PLUGIN_PATH} = $testPluginPath;
+        }
+    }
+
+    if (isQt()) {
+        $CLEAN_ENV{QTWEBKIT_PLUGIN_PATH} = productDir() . "/lib/plugins";
+        $CLEAN_ENV{QT_DRT_WEBVIEW_MODE} = $ENV{"QT_DRT_WEBVIEW_MODE"};
+    }
+    
+    my @args = ($dumpTool, @toolArgs);
+    if (isAppleMacWebKit()) { 
+        unshift @args, "arch", "-" . architecture();             
+    }
+
+    if ($useValgrind) {
+        unshift @args, "valgrind", "--suppressions=$platformBaseDirectory/qt/SuppressedValgrindErrors";
+    } 
+
+    if ($useWebKitTestRunner) {
+        # Make WebKitTestRunner use a similar timeout. We don't use the exact same timeout to avoid
+        # race conditions.
+        push @args, "--timeout", $timeoutSeconds - 5;
+    }
+
+    $CLEAN_ENV{MallocStackLogging} = 1 if $shouldCheckLeaks;
+
+    $dumpToolPID = open3(\*OUT, \*IN, \*ERROR, launchWithEnv(@args, %CLEAN_ENV)) or die "Failed to start tool: $dumpTool\n";
+    $isDumpToolOpen = 1;
+    $dumpToolCrashed = 0;
+}
+
+sub closeDumpTool()
+{
+    return if !$isDumpToolOpen;
+
+    if ($verbose && $testsPerDumpTool != 1) {
+        print "| Closing DumpTool |\n";
+    }
+
+    close IN;
+    close OUT;
+    waitpid $dumpToolPID, 0;
+    
+    # check for WebCore counter leaks.
+    if ($shouldCheckLeaks) {
+        while (<ERROR>) {
+            print;
+        }
+    }
+    close ERROR;
+    $isDumpToolOpen = 0;
+}
+
+sub dumpToolDidCrash()
+{
+    return 1 if $dumpToolCrashed;
+    return 0 unless $isDumpToolOpen;
+    my $pid = waitpid(-1, WNOHANG);
+    return 1 if ($pid == $dumpToolPID);
+
+    # On Mac OS X, crashing may be significantly delayed by crash reporter.
+    return 0 unless isAppleMacWebKit();
+
+    return DumpRenderTreeSupport::processIsCrashing($dumpToolPID);
+}
+
+sub configureAndOpenHTTPDIfNeeded()
+{
+    return if $isHttpdOpen;
+    my $absTestResultsDirectory = resolveAndMakeTestResultsDirectory();
+    my $listen = "127.0.0.1:$httpdPort";
+    my @args = (
+        "-c", "CustomLog \"$absTestResultsDirectory/access_log.txt\" common",
+        "-c", "ErrorLog \"$absTestResultsDirectory/error_log.txt\"",
+        "-C", "Listen $listen"
+    );
+
+    my @defaultArgs = getDefaultConfigForTestDirectory($testDirectory);
+    @args = (@defaultArgs, @args);
+
+    waitForHTTPDLock() if $shouldWaitForHTTPD;
+    $isHttpdOpen = openHTTPD(@args);
+}
+
+sub checkPythonVersion()
+{
+    # we have not chdir to sourceDir yet.
+    system $perlInterpreter, File::Spec->catfile(sourceDir(), qw(Tools Scripts ensure-valid-python)), "--check-only";
+    return exitStatus($?) == 0;
+}
+
+sub openWebSocketServerIfNeeded()
+{
+    return 1 if $isWebSocketServerOpen;
+    return 0 if $failedToStartWebSocketServer;
+
+    my $webSocketHandlerDir = "$testDirectory";
+    my $absTestResultsDirectory = resolveAndMakeTestResultsDirectory();
+    $webSocketServerPidFile = "$absTestResultsDirectory/websocket.pid";
+
+    my @args = (
+        "Tools/Scripts/new-run-webkit-websocketserver",
+        "--server", "start",
+        "--port", "$webSocketPort",
+        "--root", "$webSocketHandlerDir",
+        "--output-dir", "$absTestResultsDirectory",
+        "--pidfile", "$webSocketServerPidFile"
+    );
+    system "/usr/bin/python", @args;
+
+    $isWebSocketServerOpen = 1;
+    return 1;
+}
+
+sub closeWebSocketServer()
+{
+    return if !$isWebSocketServerOpen;
+
+    my @args = (
+        "Tools/Scripts/new-run-webkit-websocketserver",
+        "--server", "stop",
+        "--pidfile", "$webSocketServerPidFile"
+    );
+    system "/usr/bin/python", @args;
+    unlink "$webSocketServerPidFile";
+
+    # wss is disabled until all platforms support pyOpenSSL.
+    $isWebSocketServerOpen = 0;
+}
+
+sub fileNameWithNumber($$)
+{
+    my ($base, $number) = @_;
+    return "$base$number" if ($number > 1);
+    return $base;
+}
+
+sub processIgnoreTests($$)
+{
+    my @ignoreList = split(/\s*,\s*/, shift);
+    my $listName = shift;
+
+    my $disabledSuffix = "-disabled";
+
+    my $addIgnoredDirectories = sub {
+        return () if exists $ignoredLocalDirectories{basename($File::Find::dir)};
+        $ignoredDirectories{File::Spec->abs2rel($File::Find::dir, $testDirectory)} = 1;
+        return @_;
+    };
+    foreach my $item (@ignoreList) {
+        my $path = catfile($testDirectory, $item); 
+        if (-d $path) {
+            $ignoredDirectories{$item} = 1;
+            find({ preprocess => $addIgnoredDirectories, wanted => sub {} }, $path);
+        }
+        elsif (-f $path) {
+            $ignoredFiles{$item} = 1;
+        } elsif (-f $path . $disabledSuffix) {
+            # The test is disabled, so do nothing.
+        } else {
+            print "$listName list contained '$item', but no file of that name could be found\n";
+        }
+    }
+}
+
+sub stripExtension($)
+{
+    my ($test) = @_;
+
+    $test =~ s/\.[a-zA-Z]+$//;
+    return $test;
+}
+
+sub isTextOnlyTest($)
+{
+    my ($actual) = @_;
+    my $isText;
+    if ($actual =~ /^layer at/ms) {
+        $isText = 0;
+    } else {
+        $isText = 1;
+    }
+    return $isText;
+}
+
+sub expectedDirectoryForTest($;$;$)
+{
+    my ($base, $isText, $expectedExtension) = @_;
+
+    my @directories = @platformResultHierarchy;
+
+    my @extraPlatforms = ();
+    if (isAppleWinWebKit()) {
+        push @extraPlatforms, "mac-wk2" if $platform eq "win-wk2";
+        push @extraPlatforms, qw(mac-lion mac);
+    }
+  
+    push @directories, map { catdir($platformBaseDirectory, $_) } @extraPlatforms;
+    push @directories, $expectedDirectory;
+
+    # If we already have expected results, just return their location.
+    foreach my $directory (@directories) {
+        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,
+    # while render tree dumps should go in the least-specific platform directory.
+    return $isText ? $expectedDirectory : $platformResultHierarchy[$#platformResultHierarchy];
+}
+
+sub countFinishedTest($$$$)
+{
+    my ($test, $base, $result, $isText) = @_;
+
+    if (($count + 1) % $testsPerDumpTool == 0 || $count == $#tests) {
+        if ($shouldCheckLeaks) {
+            my $fileName;
+            if ($testsPerDumpTool == 1) {
+                $fileName = File::Spec->catfile($testResultsDirectory, "$base-leaks.txt");
+            } else {
+                $fileName = File::Spec->catfile($testResultsDirectory, fileNameWithNumber($dumpToolName, $leaksOutputFileNumber) . "-leaks.txt");
+            }
+            my $leakCount = countAndPrintLeaks($dumpToolName, $dumpToolPID, $fileName);
+            $totalLeaks += $leakCount;
+            $leaksOutputFileNumber++ if ($leakCount);
+        }
+
+        closeDumpTool();
+    }
+    
+    $count++;
+    $counts{$result}++;
+    push @{$tests{$result}}, $test;
+}
+
+sub testCrashedOrTimedOut($$$$$$)
+{
+    my ($test, $base, $didCrash, $webProcessCrashed, $actual, $error) = @_;
+
+    printFailureMessageForTest($test, $webProcessCrashed ? "Web process crashed" : $didCrash ? "crashed" : "timed out");
+
+    sampleDumpTool() unless $didCrash || $webProcessCrashed;
+
+    my $dir = dirname(File::Spec->catdir($testResultsDirectory, $base));
+    mkpath $dir;
+
+    deleteExpectedAndActualResults($base);
+
+    if (defined($error) && length($error)) {
+        writeToFile(File::Spec->catfile($testResultsDirectory, "$base-$errorTag.txt"), $error);
+    }
+
+    recordActualResultsAndDiff($base, $actual);
+
+    # There's no point in killing the dump tool when it's crashed. And it will kill itself when the
+    # web process crashes.
+    kill 9, $dumpToolPID unless $didCrash || $webProcessCrashed;
+
+    closeDumpTool();
+
+    captureSavedCrashLog($base, $webProcessCrashed) if $didCrash || $webProcessCrashed;
+
+    return unless isCygwin() && !$didCrash && $base =~ /^http/;
+    # On Cygwin, http tests timing out can be a symptom of a non-responsive httpd.
+    # If we timed out running an http test, try restarting httpd.
+    $isHttpdOpen = !closeHTTPD();
+    configureAndOpenHTTPDIfNeeded();
+}
+
+sub captureSavedCrashLog($$)
+{
+    my ($base, $webProcessCrashed) = @_;
+
+    my $crashLog;
+
+    my $glob;
+    if (isCygwin()) {
+        $glob = File::Spec->catfile($testResultsDirectory, $windowsCrashLogFilePrefix . "*.txt");
+    } elsif (isAppleMacWebKit()) {
+        $glob = File::Spec->catfile("~", "Library", "Logs", "DiagnosticReports", ($webProcessCrashed ? "WebProcess" : $dumpToolName) . "_*.crash");
+
+        # Even though the dump tool has exited, CrashReporter might still be running. We need to
+        # wait for it to exit to ensure it has saved its crash log to disk. For simplicitly, we'll
+        # assume that the ReportCrash process with the highest PID is the one we want.
+        if (my @reportCrashPIDs = sort map { /^\s*(\d+)/; $1 } grep { /ReportCrash/ } `/bin/ps x`) {
+            my $reportCrashPID = $reportCrashPIDs[$#reportCrashPIDs];
+            # We use kill instead of waitpid because ReportCrash is not one of our child processes.
+            usleep(250000) while kill(0, $reportCrashPID) > 0;
+        }
+    }
+
+    return unless $glob;
+
+    # We assume that the newest crash log in matching the glob is the one that corresponds to the crash that just occurred.
+    if (my $newestCrashLog = findNewestFileMatchingGlob($glob)) {
+        # The crash log must have been created after this script started running.
+        $crashLog = $newestCrashLog if -M $newestCrashLog < 0;
+    }
+
+    return unless $crashLog;
+
+    move($crashLog, File::Spec->catfile($testResultsDirectory, "$base-$crashLogTag.txt"));
+}
+
+sub findNewestFileMatchingGlob($)
+{
+    my ($glob) = @_;
+
+    my @paths = glob $glob;
+    return unless scalar(@paths);
+
+    my @pathsAndTimes = map { [$_, -M $_] } @paths;
+    @pathsAndTimes = sort { $b->[1] <=> $a->[1] } @pathsAndTimes;
+    return $pathsAndTimes[$#pathsAndTimes]->[0];
+}
+
+sub printFailureMessageForTest($$)
+{
+    my ($test, $description) = @_;
+
+    unless ($verbose) {
+        print "\n" unless $atLineStart;
+        print "$test -> ";
+    }
+    print "$description\n";
+    $atLineStart = 1;
+}
+
+my %cygpaths = ();
+
+sub openCygpathIfNeeded($)
+{
+    my ($options) = @_;
+
+    return unless isCygwin();
+    return $cygpaths{$options} if $cygpaths{$options} && $cygpaths{$options}->{"open"};
+
+    local (*CYGPATHIN, *CYGPATHOUT);
+    my $pid = open2(\*CYGPATHIN, \*CYGPATHOUT, "cygpath -f - $options");
+    my $cygpath =  {
+        "pid" => $pid,
+        "in" => *CYGPATHIN,
+        "out" => *CYGPATHOUT,
+        "open" => 1
+    };
+
+    $cygpaths{$options} = $cygpath;
+
+    return $cygpath;
+}
+
+sub closeCygpaths()
+{
+    return unless isCygwin();
+
+    foreach my $cygpath (values(%cygpaths)) {
+        close $cygpath->{"in"};
+        close $cygpath->{"out"};
+        waitpid($cygpath->{"pid"}, 0);
+        $cygpath->{"open"} = 0;
+
+    }
+}
+
+sub convertPathUsingCygpath($$)
+{
+    my ($path, $options) = @_;
+
+    # cygpath -f (at least in Cygwin 1.7) converts spaces into newlines. We remove spaces here and
+    # add them back in after conversion to work around this.
+    my $spaceSubstitute = "__NOTASPACE__";
+    $path =~ s/ /\Q$spaceSubstitute\E/g;
+
+    my $cygpath = openCygpathIfNeeded($options);
+    local *inFH = $cygpath->{"in"};
+    local *outFH = $cygpath->{"out"};
+    print outFH $path . "\n";
+    my $convertedPath = <inFH>;
+    chomp($convertedPath) if defined $convertedPath;
+
+    $convertedPath =~ s/\Q$spaceSubstitute\E/ /g;
+    return $convertedPath;
+}
+
+sub toCygwinPath($)
+{
+    my ($path) = @_;
+    return unless isCygwin();
+
+    return convertPathUsingCygpath($path, "-u");
+}
+
+sub toWindowsPath($)
+{
+    my ($path) = @_;
+    return unless isCygwin();
+
+    return convertPathUsingCygpath($path, "-w");
+}
+
+sub toURL($)
+{
+    my ($path) = @_;
+
+    if ($useRemoteLinksToTests) {
+        my $relativePath = File::Spec->abs2rel($path, $testDirectory);
+
+        # If the file is below the test directory then convert it into a link to the file in SVN
+        if ($relativePath !~ /^\.\.\//) {
+            my $revision = svnRevisionForDirectory($testDirectory);
+            my $svnPath = pathRelativeToSVNRepositoryRootForPath($path);
+            return "http://trac.webkit.org/export/$revision/$svnPath";
+        }
+    }
+
+    return $path unless isCygwin();
+
+    return "file:///" . convertPathUsingCygpath($path, "-m");
+}
+
+sub validateSkippedArg($$;$)
+{
+    my ($option, $value, $value2) = @_;
+    my %validSkippedValues = map { $_ => 1 } qw(default ignore only);
+    $value = lc($value);
+    die "Invalid argument '" . $value . "' for option $option" unless $validSkippedValues{$value};
+    $treatSkipped = $value;
+}
+
+sub htmlForResultsSection(\@$&)
+{
+    my ($tests, $description, $linkGetter) = @_;
+
+    my @html = ();
+    return join("\n", @html) unless @{$tests};
+
+    push @html, "<p>$description:</p>";
+    push @html, "<table>";
+    foreach my $test (@{$tests}) {
+        push @html, "<tr>";
+        push @html, "<td><a href=\"" . toURL("$testDirectory/$test") . "\">$test</a></td>";
+        foreach my $link (@{&{$linkGetter}($test)}) {
+            push @html, "<td>";
+            push @html, "<a href=\"$link->{href}\">$link->{text}</a>" if -f File::Spec->catfile($testResultsDirectory, $link->{href});
+            push @html, "</td>";
+        }
+        push @html, "</tr>";
+    }
+    push @html, "</table>";
+
+    return join("\n", @html);
+}
+
+sub linksForExpectedAndActualResults($)
+{
+    my ($base) = @_;
+
+    my @links = ();
+
+    return \@links unless -s "$testResultsDirectory/$base-$diffsTag.txt";
+    
+    my $expectedResultPath = $expectedResultPaths{$base};
+    my ($expectedResultFileName, $expectedResultsDirectory, $expectedResultExtension) = fileparse($expectedResultPath, qr{\.[^.]+$});
+
+    push @links, { href => "$base-$expectedTag$expectedResultExtension", text => "expected" };
+    push @links, { href => "$base-$actualTag$expectedResultExtension", text => "actual" };
+    push @links, { href => "$base-$diffsTag.txt", text => "diff" };
+    push @links, { href => "$base-$prettyDiffTag.html", text => "pretty diff" };
+
+    return \@links;
+}
+
+sub linksForMismatchTest
+{
+    my ($test) = @_;
+
+    my @links = ();
+
+    my $base = stripExtension($test);
+
+    push @links, @{linksForExpectedAndActualResults($base)};
+    return \@links unless $pixelTests && $imagesPresent{$base};
+
+    push @links, { href => "$base-$expectedTag.png", text => "expected image" };
+    push @links, { href => "$base-$diffsTag.html", text => "image diffs" };
+    push @links, { href => "$base-$diffsTag.png", text => "$imageDifferences{$base}%" };
+
+    return \@links;
+}
+
+sub crashLocation($)
+{
+    my ($base) = @_;
+
+    my $crashLogFile = File::Spec->catfile($testResultsDirectory, "$base-$crashLogTag.txt");
+
+    if (isCygwin()) {
+        # We're looking for the following text:
+        #
+        # FOLLOWUP_IP:
+        # module!function+offset [file:line]
+        #
+        # The second contains the function that crashed (or the function that ended up jumping to a bad
+        # address, as in the case of a null function pointer).
+
+        open LOG, "<", $crashLogFile or return;
+        while (my $line = <LOG>) {
+            last if $line =~ /^FOLLOWUP_IP:/;
+        }
+        my $desiredLine = <LOG>;
+        close LOG;
+
+        return unless $desiredLine;
+
+        # Just take everything up to the first space (which is where the file/line information should
+        # start).
+        $desiredLine =~ /^(\S+)/;
+        return $1;
+    }
+
+    if (isAppleMacWebKit()) {
+        # We're looking for the following text:
+        #
+        # Thread M Crashed:
+        # N   module                              address function + offset (file:line)
+        #
+        # Some lines might have a module of "???" if we've jumped to a bad address. We should skip
+        # past those.
+
+        open LOG, "<", $crashLogFile or return;
+        while (my $line = <LOG>) {
+            last if $line =~ /^Thread \d+ Crashed:/;
+        }
+        my $location;
+        while (my $line = <LOG>) {
+            $line =~ /^\d+\s+(\S+)\s+\S+ (.* \+ \d+)/ or next;
+            my $module = $1;
+            my $functionAndOffset = $2;
+            next if $module eq "???";
+            $location = "$module: $functionAndOffset";
+            last;
+        }
+        close LOG;
+        return $location;
+    }
+}
+
+sub linksForErrorTest
+{
+    my ($test) = @_;
+
+    my @links = ();
+
+    my $base = stripExtension($test);
+
+    my $crashLogText = "crash log";
+    if (my $crashLocation = crashLocation($base)) {
+        $crashLogText .= " (<code>" . CGI::escapeHTML($crashLocation) . "</code>)";
+    }
+
+    push @links, @{linksForExpectedAndActualResults($base)};
+    push @links, { href => "$base-$errorTag.txt", text => "stderr" };
+    push @links, { href => "$base-$crashLogTag.txt", text => $crashLogText };
+
+    return \@links;
+}
+
+sub linksForNewTest
+{
+    my ($test) = @_;
+
+    my @links = ();
+
+    my $base = stripExtension($test);
+
+    my $expectedResultPath = $expectedResultPaths{$base};
+    my ($expectedResultFileName, $expectedResultsDirectory, $expectedResultExtension) = fileparse($expectedResultPath, qr{\.[^.]+$});
+
+    push @links, { href => "$base-$actualTag$expectedResultExtension", text => "result" };
+    if ($pixelTests && $imagesPresent{$base}) {
+        push @links, { href => "$base-$expectedTag.png", text => "image" };
+    }
+
+    return \@links;
+}
+
+sub deleteExpectedAndActualResults($)
+{
+    my ($base) = @_;
+
+    unlink "$testResultsDirectory/$base-$actualTag.txt";
+    unlink "$testResultsDirectory/$base-$diffsTag.txt";
+    unlink "$testResultsDirectory/$base-$errorTag.txt";
+    unlink "$testResultsDirectory/$base-$crashLogTag.txt";
+}
+
+sub recordActualResultsAndDiff($$)
+{
+    my ($base, $actualResults) = @_;
+
+    return unless defined($actualResults) && length($actualResults);
+
+    my $expectedResultPath = $expectedResultPaths{$base};
+    my ($expectedResultFileNameMinusExtension, $expectedResultDirectoryPath, $expectedResultExtension) = fileparse($expectedResultPath, qr{\.[^.]+$});
+    my $actualResultsPath = File::Spec->catfile($testResultsDirectory, "$base-$actualTag$expectedResultExtension");
+    my $copiedExpectedResultsPath = File::Spec->catfile($testResultsDirectory, "$base-$expectedTag$expectedResultExtension");
+
+    mkpath(dirname($actualResultsPath));
+    writeToFile("$actualResultsPath", $actualResults);
+
+    # We don't need diff and pretty diff for tests without expected file.
+    if ( !-f $expectedResultPath) {
+        return;
+    }
+
+    copy("$expectedResultPath", "$copiedExpectedResultsPath");
+
+    my $diffOuputBasePath = File::Spec->catfile($testResultsDirectory, $base);
+    my $diffOutputPath = "$diffOuputBasePath-$diffsTag.txt";
+    system "diff -u \"$copiedExpectedResultsPath\" \"$actualResultsPath\" > \"$diffOutputPath\"";
+
+    my $prettyDiffOutputPath = "$diffOuputBasePath-$prettyDiffTag.html";
+    my $prettyPatchPath = "Websites/bugs.webkit.org/PrettyPatch/";
+    my $prettifyPath = "$prettyPatchPath/prettify.rb";
+    system "ruby -I \"$prettyPatchPath\" \"$prettifyPath\" \"$diffOutputPath\" > \"$prettyDiffOutputPath\"";
+}
+
+sub buildPlatformResultHierarchy()
+{
+    mkpath($platformTestDirectory) if ($platform eq "undefined" && !-d "$platformTestDirectory");
+
+    my @platforms;
+    
+    my $isMac = $platform =~ /^mac/;
+    my $isWin = $platform =~ /^win/;
+    if ($isMac || $isWin) {
+        my $effectivePlatform = $platform;
+        if ($platform eq "mac-wk2" || $platform eq "win-wk2") {
+            push @platforms, $platform;
+            $effectivePlatform = $realPlatform;
+        }
+
+        my @platformList = $isMac ? @macPlatforms : @winPlatforms;
+        my $i;
+        for ($i = 0; $i < @platformList; $i++) {
+            last if $platformList[$i] eq $effectivePlatform;
+        }
+        for (; $i < @platformList; $i++) {
+            push @platforms, $platformList[$i];
+        }
+
+        if ($platform eq "wincairo") {
+            @platforms = $platform;
+        }
+    } elsif ($platform =~ /^qt/) {
+        if ($platform eq "qt-5.0-wk2" || getQtVersion() eq "5.0" && $useWebKitTestRunner) {
+            push @platforms, "qt-5.0-wk2";
+        }
+        elsif ($platform eq "qt-5.0-wk1" || getQtVersion() eq "5.0" && !$useWebKitTestRunner) {
+            push @platforms, "qt-5.0-wk1"
+        }
+
+        if (isARM() || $platform eq "qt-arm") {
+            push @platforms, "qt-arm";
+        }
+
+        if (isDarwin() || $platform eq "qt-mac") {
+            push @platforms, "qt-mac";
+        }
+        elsif (isWindows() || isCygwin() || $platform eq "qt-win") {
+            push @platforms, "qt-win";
+        }
+        elsif (isLinux() || $platform eq "qt-linux") {
+            push @platforms, "qt-linux";
+        }
+
+        if (getQtVersion() eq "4.8" || $platform eq "qt-4.8") {
+            push @platforms, "qt-4.8";
+        }
+        elsif (getQtVersion() eq "5.0" || $platform eq "qt-5.0") {
+            push @platforms, "qt-5.0";
+        }
+
+        push @platforms, "qt";
+    } elsif ($platform =~ /^gtk-/) {
+        push @platforms, $platform;
+        push @platforms, "gtk";
+    } else {
+        @platforms = $platform;
+    }
+
+    my @hierarchy;
+    for (my $i = 0; $i < @platforms; $i++) {
+        my $scoped = catdir($platformBaseDirectory, $platforms[$i]);
+        push(@hierarchy, $scoped) if (-d $scoped);
+    }
+    
+    unshift @hierarchy, grep { -d $_ } @additionalPlatformDirectories;
+
+    return @hierarchy;
+}
+
+sub buildPlatformTestHierarchy(@)
+{
+    my (@platformHierarchy) = @_;
+
+    my @result;
+    if ($platform =~ /^qt/) {
+        for (my $i = 0; $i < @platformHierarchy; ++$i) {
+           push @result, $platformHierarchy[$i];
+        }
+    } else {
+        my $wk2Platform;
+        for (my $i = 0; $i < @platformHierarchy; ++$i) {
+            if ($platformHierarchy[$i] =~ /-wk2/) {
+                $wk2Platform = splice @platformHierarchy, $i, 1;
+                last;
+            }
+        }
+
+       push @result, $platformHierarchy[0];
+       push @result, $wk2Platform if defined $wk2Platform;
+       push @result, $platformHierarchy[$#platformHierarchy] if @platformHierarchy >= 2;
+    }
+
+    if ($verbose) {
+        my @searchPaths;
+        foreach my $searchPath (@result) {
+            my ($dir, $name) = splitpath($searchPath);
+            push @searchPaths, $name;
+        }
+        my $searchPathHierarchy = join(' -> ', @searchPaths);
+        print "Baseline search path: $searchPathHierarchy\n";
+    }
+
+    return @result;
+}
+
+sub epiloguesAndPrologues($$)
+{
+    my ($lastDirectory, $directory) = @_;
+    my @lastComponents = split('/', $lastDirectory);
+    my @components = split('/', $directory);
+
+    while (@lastComponents) {
+        if (!defined($components[0]) || $lastComponents[0] ne $components[0]) {
+            last;
+        }
+        shift @components;
+        shift @lastComponents;
+    }
+
+    my @result;
+    my $leaving = $lastDirectory;
+    foreach (@lastComponents) {
+        my $epilogue = $leaving . "/resources/run-webkit-tests-epilogue.html";
+        foreach (@platformResultHierarchy) {
+            push @result, catdir($_, $epilogue) if (stat(catdir($_, $epilogue)));
+        }
+        push @result, catdir($testDirectory, $epilogue) if (stat(catdir($testDirectory, $epilogue)));
+        $leaving =~ s|(^\|/)[^/]+$||;
+    }
+
+    my $entering = $leaving;
+    foreach (@components) {
+        $entering .= '/' . $_;
+        my $prologue = $entering . "/resources/run-webkit-tests-prologue.html";
+        push @result, catdir($testDirectory, $prologue) if (stat(catdir($testDirectory, $prologue)));
+        foreach (reverse @platformResultHierarchy) {
+            push @result, catdir($_, $prologue) if (stat(catdir($_, $prologue)));
+        }
+    }
+    return @result;
+}
+    
+sub parseLeaksandPrintUniqueLeaks()
+{
+    return unless @leaksFilenames;
+
+    my $mergedFilenames = join " ", @leaksFilenames;
+    my $parseMallocHistoryTool = sourceDir() . "/Tools/Scripts/parse-malloc-history";
+    
+    open MERGED_LEAKS, "cat $mergedFilenames | $parseMallocHistoryTool --merge-depth $mergeDepth  - |" ;
+    my @leakLines = <MERGED_LEAKS>;
+    close MERGED_LEAKS;
+    
+    my $uniqueLeakCount = 0;
+    my $totalBytes;
+    foreach my $line (@leakLines) {
+        ++$uniqueLeakCount if ($line =~ /^(\d*)\scalls/);
+        $totalBytes = $1 if $line =~ /^total\:\s(.*)\s\(/;
+    }
+    
+    print "\nWARNING: $totalLeaks total leaks found for a total of $totalBytes!\n";
+    print "WARNING: $uniqueLeakCount unique leaks found!\n";
+    print "See above for individual leaks results.\n" if ($leaksOutputFileNumber > 2);
+    
+}
+
+sub extensionForMimeType($)
+{
+    my ($mimeType) = @_;
+
+    if ($mimeType eq "application/x-webarchive") {
+        return "webarchive";
+    } elsif ($mimeType eq "application/pdf") {
+        return "pdf";
+    } elsif ($mimeType eq "audio/wav") {
+        return "wav";
+    }
+    return "txt";
+}
+
+# Read up to the first #EOF (the content block of the test), or until detecting crashes or timeouts.
+sub readFromDumpToolWithTimer(**)
+{
+    my ($fhIn, $fhError) = @_;
+
+    setFileHandleNonBlocking($fhIn, 1);
+    setFileHandleNonBlocking($fhError, 1);
+
+    my $maximumSecondsWithoutOutput = $timeoutSeconds;
+    my $microsecondsToWaitBeforeReadingAgain = 1000;
+
+    my $timeOfLastSuccessfulRead = time;
+
+    my @output = ();
+    my @error = ();
+    my $status = "success";
+    my $mimeType = "text/plain";
+    my $encoding = "";
+    # We don't have a very good way to know when the "headers" stop
+    # and the content starts, so we use this as a hack:
+    my $haveSeenContentType = 0;
+    my $haveSeenContentTransferEncoding = 0;
+    my $haveSeenEofIn = 0;
+    my $haveSeenEofError = 0;
+
+    while (1) {
+        if (time - $timeOfLastSuccessfulRead > $maximumSecondsWithoutOutput) {
+            $status = dumpToolDidCrash() ? "crashed" : "timedOut";
+            last;
+        }
+
+        # Once we've seen the EOF, we must not read anymore.
+        my $lineIn = readline($fhIn) unless $haveSeenEofIn;
+        my $lineError = readline($fhError) unless $haveSeenEofError;
+        if (!defined($lineIn) && !defined($lineError)) {
+            last if ($haveSeenEofIn && $haveSeenEofError);
+
+            if ($! != EAGAIN) {
+                $status = "crashed";
+                last;
+            }
+
+            # No data ready
+            usleep($microsecondsToWaitBeforeReadingAgain);
+            next;
+        }
+
+        $timeOfLastSuccessfulRead = time;
+
+        if (defined($lineIn)) {
+            if (!$haveSeenContentType && $lineIn =~ /^Content-Type: (\S+)$/) {
+                $mimeType = $1;
+                $haveSeenContentType = 1;
+            } elsif (!$haveSeenContentTransferEncoding && $lineIn =~ /^Content-Transfer-Encoding: (\S+)$/) {
+                $encoding = $1;
+                $haveSeenContentTransferEncoding = 1;
+            } elsif ($lineIn =~ /^DumpMalloc|^DumpJSHeap: (\S+)$/) {
+                    # Ignored. Only used in performance tests.
+            } elsif ($lineIn =~ /(.*)#EOF$/) {
+                if ($1 ne "") {
+                    push @output, $1;
+                }
+                $haveSeenEofIn = 1;
+            } else {
+                push @output, $lineIn;
+            }
+        }
+        if (defined($lineError)) {
+            if ($lineError =~ /#CRASHED - WebProcess/) {
+                $status = "webProcessCrashed";
+                last;
+            }
+            if ($lineError =~ /#CRASHED/) {
+                $status = "crashed";
+                last;
+            }
+            if ($lineError =~ /#EOF/) {
+                $haveSeenEofError = 1;
+            } else {
+                push @error, $lineError;
+            }
+        }
+    }
+
+    setFileHandleNonBlocking($fhIn, 0);
+    setFileHandleNonBlocking($fhError, 0);
+    my $joined_output = join("", @output);
+    if ($encoding eq "base64") {
+        $joined_output = decode_base64($joined_output);
+    }
+    return {
+        output => $joined_output,
+        error => join("", @error),
+        status => $status,
+        mimeType => $mimeType,
+        extension => extensionForMimeType($mimeType)
+    };
+}
+
+sub setFileHandleNonBlocking(*$)
+{
+    my ($fh, $nonBlocking) = @_;
+
+    my $flags = fcntl($fh, F_GETFL, 0) or die "Couldn't get filehandle flags";
+
+    if ($nonBlocking) {
+        $flags |= O_NONBLOCK;
+    } else {
+        $flags &= ~O_NONBLOCK;
+    }
+
+    fcntl($fh, F_SETFL, $flags) or die "Couldn't set filehandle flags";
+
+    return 1;
+}
+
+sub sampleDumpTool()
+{
+    return unless isAppleMacWebKit();
+    return unless $runSample;
+
+    my $outputDirectory = "$ENV{HOME}/Library/Logs/DumpRenderTree";
+    -d $outputDirectory or mkdir $outputDirectory;
+
+    my $outputFile = "$outputDirectory/HangReport.txt";
+    system "/usr/bin/sample", $dumpToolPID, qw(10 10 -file), $outputFile;
+}
+
+sub stripMetrics($$)
+{
+    my ($actual, $expected) = @_;
+
+    foreach my $result ($actual, $expected) {
+        $result =~ s/at \(-?[0-9]+,-?[0-9]+\) *//g;
+        $result =~ s/size -?[0-9]+x-?[0-9]+ *//g;
+        $result =~ s/text run width -?[0-9]+: //g;
+        $result =~ s/text run width -?[0-9]+ [a-zA-Z ]+: //g;
+        $result =~ s/RenderButton {BUTTON} .*/RenderButton {BUTTON}/g;
+        $result =~ s/RenderImage {INPUT} .*/RenderImage {INPUT}/g;
+        $result =~ s/RenderBlock {INPUT} .*/RenderBlock {INPUT}/g;
+        $result =~ s/RenderTextControl {INPUT} .*/RenderTextControl {INPUT}/g;
+        $result =~ s/\([0-9]+px/px/g;
+        $result =~ s/ *" *\n +" */ /g;
+        $result =~ s/" +$/"/g;
+        $result =~ s/- /-/g;
+        $result =~ s/\n( *)"\s+/\n$1"/g;
+        $result =~ s/\s+"\n/"\n/g;
+        $result =~ s/scrollWidth [0-9]+/scrollWidth/g;
+        $result =~ s/scrollHeight [0-9]+/scrollHeight/g;
+        $result =~ s/scrollX [0-9]+/scrollX/g;
+        $result =~ s/scrollY [0-9]+/scrollY/g;
+        $result =~ s/scrolled to [0-9]+,[0-9]+/scrolled/g;
+    }
+
+    return ($actual, $expected);
+}
+
+sub fileShouldBeIgnored
+{
+    my ($filePath) = @_;
+    foreach my $ignoredDir (keys %ignoredDirectories) {
+        if ($filePath =~ m/^$ignoredDir/) {
+            return 1;
+        }
+    }
+    return 0;
+}
+
+sub readSkippedFiles($)
+{
+    my ($constraintPath) = @_;
+
+    my @skippedFileDirectories = @platformTestHierarchy;
+
+    # Because nearly all of the skipped tests for WebKit 2 on Mac are due to
+    # cross-platform issues, the Windows and Qt ports use the Mac skipped list
+    # additionally to their own to avoid maintaining separate lists.
+    push(@skippedFileDirectories, catdir($platformBaseDirectory, "wk2")) if ($platform eq "win-wk2" || $platform eq "qt-5.0-wk2" || $platform eq "mac-wk2" || $platform eq "gtk-wk2");
+
+    if ($verbose) {
+        foreach my $skippedPath (@skippedFileDirectories) {
+            print "Using Skipped file: $skippedPath\n";
+        }
+    }
+
+    foreach my $level (@skippedFileDirectories) {
+        # If a Skipped file exists in the directory, use that and ignore the TestExpectations file,
+        # but if it doesn't, treat every entry in the TestExpectations file as if it should be Skipped.
+        if (open SKIPPED, "<", "$level/Skipped") {
+            if ($verbose) {
+                my ($dir, $name) = splitpath($level);
+                print "Skipped tests in $name:\n";
+            }
+
+            while (<SKIPPED>) {
+                my $skipped = $_;
+                chomp $skipped;
+                $skipped =~ s/^[ \n\r]+//;
+                $skipped =~ s/[ \n\r]+$//;
+                if ($skipped && $skipped !~ /^#/) {
+                    processSkippedFileEntry($skipped, "Skipped", $constraintPath);
+                }
+            }
+            close SKIPPED;
+        } elsif (open EXPECTATIONS, "<", "$level/TestExpectations") {
+            if ($verbose) {
+                my ($dir, $name) = splitpath($level);
+                print "Skipping tests from $name:\n";
+            }
+            LINE: while (<EXPECTATIONS>) {
+                my $line = $_;
+                chomp $line;
+                $line =~ s/^[ \n\r]+//;
+                $line =~ s/[ \n\r]+$//;
+                $line =~ s/#.*$//;
+
+                # This logic roughly mirrors the logic in test_expectations.py _tokenize_line() but we
+                # don't bother to look at any of the modifiers or expectations and just skip everything.
+
+                my $state = "start";
+                my $skipped = "";
+                TOKEN: foreach my $token (split(/\s+/, $line)) {
+                    if (startsWith($token, "BUG") || startsWith($token, "//")) {
+                        # Ignore any lines with the old-style syntax.
+                        next LINE;
+                    }
+                    if (startsWith($token, "webkit.org/b/") || startsWith($token, "Bug(")) {
+                        # Skip over bug identifiers; note that we don't bother looking for
+                        # Chromium or V8 URLs since ORWT doesn't work with Chromium.
+                        next TOKEN;
+                    } elsif ($token eq "[") {
+                        if ($state eq 'start') {
+                            $state = 'configuration';
+                        }
+                    } elsif ($token eq "]") {
+                        if ($state eq 'configuration') {
+                            $state = 'name';
+                        }
+                    } elsif (($state eq "name") || ($state eq "start")) {
+                        $skipped = $token;
+                        # Skip over the rest of the line.
+                        last TOKEN;
+                    }
+                }
+                if ($skipped) {
+                    processSkippedFileEntry($skipped, "TestExpectations", $constraintPath);
+                }
+            }
+            close EXPECTATIONS;
+        }
+    }
+}
+
+sub processSkippedFileEntry($$$)
+{
+    my ($skipped, $listname, $constraintPath) = @_;
+
+    if ($skippedOnly) {
+        if (!fileShouldBeIgnored($skipped)) {
+            if (!$constraintPath) {
+                # Always add $skipped since no constraint path was specified on the command line.
+                push(@ARGV, $skipped);
+            } elsif ($skipped =~ /^($constraintPath)/ || ("LayoutTests/".$skipped) =~ /^($constraintPath)/ ) {
+                # Add $skipped only if it matches the current path constraint, e.g.,
+                # "--skipped=only dir1" with "dir1/file1.html" on the skipped list or
+                # "--skipped=only LayoutTests/dir1" with "dir1/file1.html" on the skipped list
+                push(@ARGV, $skipped);
+            } elsif ($constraintPath =~ /^("LayoutTests\/".$skipped)/ || $constraintPath =~ /^($skipped)/) {
+                # Add current path constraint if it is more specific than the skip list entry,
+                # e.g., "--skipped=only dir1/dir2/dir3" with "dir1" on the skipped list or
+                # e.g., "--skipped=only LayoutTests/dir1/dir2/dir3" with "dir1" on the skipped list.
+                push(@ARGV, $constraintPath);
+            }
+        } elsif ($verbose) {
+            print "    $skipped\n";
+        }
+    } else {
+        if ($verbose) {
+            print "    $skipped\n";
+        }
+        processIgnoreTests($skipped, $listname);
+    }
+}
+
+sub startsWith($$)
+{
+    my ($string, $substring) = @_;
+    return index($string, $substring) == 0;
+}
+
+sub readChecksumFromPng($)
+{
+    my ($path) = @_;
+    my $data;
+    if (open(PNGFILE, $path) && read(PNGFILE, $data, 2048) && $data =~ /tEXtchecksum\0([a-fA-F0-9]{32})/) {
+        return $1;
+    }
+}
+
+my @testsFound;
+
+sub isUsedInReftest($)
+{
+    my $filename = $_[0];
+    my @extensions = ('html','shtml','xml','xhtml','htm','php','svg','mht','pl');
+    my $extensionsJoined = join("|", @extensions);
+    my $extensionExpression = "-$expectedTag(-$mismatchTag)?\\.(".$extensionsJoined.")\$";
+    
+    if ($filename =~ /$extensionExpression/) {
+        return 1;
+    }
+    my $base = stripExtension($filename);
+    
+    foreach my $extension (@extensions) {
+        if (-f "$base-$expectedTag.$extension" || -f "$base-$expectedTag-$mismatchTag.$extension") {
+            return 1;
+        }
+    }
+    return 0;
+}
+
+sub directoryFilter
+{
+    return () if exists $ignoredLocalDirectories{basename($File::Find::dir)};
+    return () if exists $ignoredDirectories{File::Spec->abs2rel($File::Find::dir, $testDirectory)};
+    return @_;
+}
+
+sub fileFilter
+{
+    my $filename = $_;
+    if ($filename =~ /\.([^.]+)$/) {
+        my $extension = $1;
+        if (exists $supportedFileExtensions{$extension} && !isUsedInReftest($filename)) {
+            my $path = File::Spec->abs2rel(catfile($File::Find::dir, $filename), $testDirectory);
+            push @testsFound, $path if !exists $ignoredFiles{$path};
+        }
+    }
+}
+
+sub findTestsToRun
+{
+    my @testsToRun = ();
+
+    for my $test (@ARGV) {
+        $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";
+        } elsif (-f $fullPath && !isUsedInReftest($fullPath)) {
+            my ($filename, $pathname, $fileExtension) = fileparse($test, qr{\.[^.]+$});
+            if (!exists $supportedFileExtensions{substr($fileExtension, 1)}) {
+                print "test $test does not have a supported extension\n";
+            } elsif ($testHTTP || $pathname !~ /^http\//) {
+                push @testsToRun, $test;
+            }
+        } elsif (-d $fullPath) {
+            @testsFound = ();
+            find({ preprocess => \&directoryFilter, wanted => \&fileFilter }, $fullPath);
+            for my $level (@platformTestHierarchy) {
+                my $platformPath = catfile($level, $test);
+                find({ preprocess => \&directoryFilter, wanted => \&fileFilter }, $platformPath) if (-d $platformPath);
+            }
+            push @testsToRun, sort pathcmp @testsFound;
+            @testsFound = ();
+        } else {
+            print "test $test not found\n";
+        }
+    }
+
+    if (!scalar @ARGV) {
+        @testsFound = ();
+        find({ preprocess => \&directoryFilter, wanted => \&fileFilter }, $testDirectory);
+        for my $level (@platformTestHierarchy) {
+            find({ preprocess => \&directoryFilter, wanted => \&fileFilter }, $level);
+        }
+        push @testsToRun, sort pathcmp @testsFound;
+        @testsFound = ();
+
+        # We need to minimize the time when Apache and WebSocketServer is locked by tests
+        # so run them last if no explicit order was specified in the argument list.
+        my @httpTests;
+        my @websocketTests;
+        my @otherTests;
+        foreach my $test (@testsToRun) {
+            if ($test =~ /^http\//) {
+                push(@httpTests, $test);
+            } elsif ($test =~ /^websocket\//) {
+                push(@websocketTests, $test);
+            } else {
+                push(@otherTests, $test);
+            }
+        }
+        @testsToRun = (@otherTests, @httpTests, @websocketTests);
+    }
+
+    # Reverse the tests
+    @testsToRun = reverse @testsToRun if $reverseTests;
+
+    # Shuffle the array
+    @testsToRun = shuffle(@testsToRun) if $randomizeTests;
+
+    return @testsToRun;
+}
+
+sub printResults
+{
+    my %text = (
+        match => "succeeded",
+        mismatch => "had incorrect layout",
+        new => "were new",
+        timedout => "timed out",
+        crash => "crashed",
+        webProcessCrash => "Web process crashed",
+        error => "had stderr output"
+    );
+
+    for my $type ("match", "mismatch", "new", "timedout", "crash", "webProcessCrash", "error") {
+        my $typeCount = $counts{$type};
+        next unless $typeCount;
+        my $typeText = $text{$type};
+        my $message;
+        if ($typeCount == 1) {
+            $typeText =~ s/were/was/;
+            $message = sprintf "1 test case %s\n", $typeText;
+        } else {
+            $message = sprintf "%d test cases %s\n", $typeCount, $typeText;
+        }
+        $message =~ s-\(0%\)-(<1%)-;
+        print $message;
+    }
+}
+
+sub stopRunningTestsEarlyIfNeeded()
+{
+    # --reset-results does not check pass vs. fail, so exitAfterNFailures makes no sense with --reset-results.
+    return 0 if $resetResults;
+
+    my $passCount = $counts{match} || 0; # $counts{match} will be undefined if we've not yet passed a test (e.g. the first test fails).
+    my $newCount = $counts{new} || 0;
+    my $failureCount = $count - $passCount - $newCount; # "Failure" here includes timeouts, crashes, etc.
+    if ($exitAfterNFailures && $failureCount >= $exitAfterNFailures) {
+        $stoppedRunningEarlyMessage = "Exiting early after $failureCount failures. $count tests run.";
+        print "\n", $stoppedRunningEarlyMessage;
+        closeDumpTool();
+        return 1;
+    }
+
+    my $crashCount = $counts{crash} || 0;
+    my $webProcessCrashCount = $counts{webProcessCrash} || 0;
+    my $timeoutCount = $counts{timedout} || 0;
+    if ($exitAfterNCrashesOrTimeouts && $crashCount + $webProcessCrashCount + $timeoutCount >= $exitAfterNCrashesOrTimeouts) {
+        $stoppedRunningEarlyMessage = "Exiting early after $crashCount crashes, $webProcessCrashCount web process crashes, and $timeoutCount timeouts. $count tests run.";
+        print "\n", $stoppedRunningEarlyMessage;
+        closeDumpTool();
+        return 1;
+    }
+
+    return 0;
+}
+
+# Store this at global scope so it won't be GCed (and thus unlinked) until the program exits.
+my $debuggerTempDirectory;
+
+sub createDebuggerCommandFile()
+{
+    return unless isCygwin();
+
+    my @commands = (
+        '.logopen /t "' . toWindowsPath($testResultsDirectory) . "\\" . $windowsCrashLogFilePrefix . '.txt"',
+        '.srcpath "' . toWindowsPath(sourceDir()) . '"',
+        '!analyze -vv',
+        '~*kpn',
+        'q',
+    );
+
+    $debuggerTempDirectory = File::Temp->newdir;
+
+    my $commandFile = File::Spec->catfile($debuggerTempDirectory, "debugger-commands.txt");
+    unless (open COMMANDS, '>', $commandFile) {
+        print "Failed to open $commandFile. Crash logs will not be saved.\n";
+        return;
+    }
+    print COMMANDS join("\n", @commands), "\n";
+    unless (close COMMANDS) {
+        print "Failed to write to $commandFile. Crash logs will not be saved.\n";
+        return;
+    }
+
+    return $commandFile;
+}
+
+sub setUpWindowsCrashLogSaving()
+{
+    return unless isCygwin();
+
+    unless (defined $ENV{_NT_SYMBOL_PATH}) {
+        print "The _NT_SYMBOL_PATH environment variable is not set. Crash logs will not be saved.\nSee <http://trac.webkit.org/wiki/BuildingOnWindows#GettingCrashLogs>.\n";
+        return;
+    }
+
+    my $ntsdPath = File::Spec->catfile(toCygwinPath($ENV{PROGRAMFILES}), "Debugging Tools for Windows (x86)", "ntsd.exe");
+    unless (-f $ntsdPath) {
+        $ntsdPath = File::Spec->catfile(toCygwinPath($ENV{ProgramW6432}), "Debugging Tools for Windows (x64)", "ntsd.exe");
+        unless (-f $ntsdPath) {
+            $ntsdPath = File::Spec->catfile(toCygwinPath($ENV{SYSTEMROOT}), "system32", "ntsd.exe");
+            unless (-f $ntsdPath) {
+                print STDERR "Can't find ntsd.exe. Crash logs will not be saved.\nSee <http://trac.webkit.org/wiki/BuildingOnWindows#GettingCrashLogs>.\n";
+                return;
+            }
+        }
+    }
+
+    # If we used -c (instead of -cf) we could pass the commands directly on the command line. But
+    # when the commands include multiple quoted paths (e.g., for .logopen and .srcpath), Windows
+    # fails to invoke the post-mortem debugger at all (perhaps due to a bug in Windows's command
+    # line parsing). So we save the commands to a file instead and tell the debugger to execute them
+    # using -cf.
+    my $commandFile = createDebuggerCommandFile() or return;
+
+    my @options = (
+        '-p %ld',
+        '-e %ld',
+        '-g',
+        '-lines',
+        '-cf "' . toWindowsPath($commandFile) . '"',
+    );
+
+    my %values = (
+        Debugger => '"' . toWindowsPath($ntsdPath) . '" ' . join(' ', @options),
+        Auto => 1
+    );
+
+    foreach my $value (keys %values) {
+        $previousWindowsPostMortemDebuggerValues{$value} = readRegistryString("$windowsPostMortemDebuggerKey/$value");
+        next if writeRegistryString("$windowsPostMortemDebuggerKey/$value", $values{$value});
+
+        print "Failed to set \"$windowsPostMortemDebuggerKey/$value\". Crash logs will not be saved.\nSee <http://trac.webkit.org/wiki/BuildingOnWindows#GettingCrashLogs>.\n";
+        return;
+    }
+
+    print "Crash logs will be saved to $testResultsDirectory.\n";
+}
+
+END {
+    return unless isCygwin();
+
+    foreach my $value (keys %previousWindowsPostMortemDebuggerValues) {
+        next if writeRegistryString("$windowsPostMortemDebuggerKey/$value", $previousWindowsPostMortemDebuggerValues{$value});
+        print "Failed to restore \"$windowsPostMortemDebuggerKey/$value\" to its previous value \"$previousWindowsPostMortemDebuggerValues{$value}\"\n.";
+    }
+}
diff --git a/Tools/Scripts/parallelcl b/Tools/Scripts/parallelcl
new file mode 100755
index 0000000..8a46365
--- /dev/null
+++ b/Tools/Scripts/parallelcl
@@ -0,0 +1,224 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use File::Basename;
+use File::Spec;
+use File::Temp;
+use POSIX;
+
+sub makeJob(\@$);
+sub forkAndCompileFiles(\@$);
+sub Exec($);
+sub waitForChild(\@);
+sub cleanup(\@);
+
+my $debug = 0;
+
+chomp(my $clexe = `cygpath -u '$ENV{'VS80COMNTOOLS'}/../../VC/bin/cl.exe'`);
+
+if ($debug) {
+    print STDERR "Received " . @ARGV . " arguments:\n";
+    foreach my $arg (@ARGV) {
+        print STDERR "$arg\n";
+    }
+}
+
+my $commandFile;
+foreach my $arg (@ARGV) {
+    if ($arg =~ /^[\/-](E|EP|P)$/) {
+        print STDERR "The invoking process wants preprocessed source, so let's hand off this whole command to the real cl.exe\n" if $debug;
+        Exec("\"$clexe\" \"" . join('" "', @ARGV) . "\"");
+    } elsif ($arg =~ /^@(.*)$/) {
+        chomp($commandFile = `cygpath -u '$1'`);
+    }
+}
+
+die "No command file specified!" unless $commandFile;
+die "Couldn't find $commandFile!" unless -f $commandFile;
+
+my @sources;
+
+open(COMMAND, '<:raw:encoding(UTF16-LE):crlf:utf8', $commandFile) or die "Couldn't open $commandFile!";
+
+# The first line of the command file contains all the options to cl.exe plus the first (possibly quoted) filename
+my $firstLine = <COMMAND>;
+$firstLine =~ s/\r?\n$//;
+
+# To find the start of the first filename, look for either the last space on the line.
+# If the filename is quoted, the last character on the line will be a quote, so look for the quote before that.
+my $firstFileIndex;
+print STDERR "Last character of first line = '" . substr($firstLine, -1, 1) . "'\n" if $debug;
+if (substr($firstLine, -1, 1) eq '"') {
+    print STDERR "First file is quoted\n" if $debug;
+    $firstFileIndex = rindex($firstLine, '"', length($firstLine) - 2);
+} else {
+    print STDERR "First file is NOT quoted\n" if $debug;
+    $firstFileIndex = rindex($firstLine, ' ') + 1;
+}
+
+my $options = substr($firstLine, 0, $firstFileIndex) . join(' ', @ARGV[1 .. $#ARGV]);
+my $possibleFirstFile = substr($firstLine, $firstFileIndex);
+if ($possibleFirstFile =~ /\.(cpp|c)/) {
+    push(@sources, $possibleFirstFile);
+} else {
+    $options .= " $possibleFirstFile";
+}
+
+print STDERR "######## Found options $options ##########\n" if $debug;
+print STDERR "####### Found first source file $sources[0] ########\n" if @sources && $debug;
+
+# The rest of the lines of the command file just contain source files, one per line
+while (my $source = <COMMAND>) {
+    chomp($source);
+    $source =~ s/^\s+//;
+    $source =~ s/\s+$//;
+    push(@sources, $source) if length($source);
+}
+close(COMMAND);
+
+my $numSources = @sources;
+exit unless $numSources > 0;
+
+my $numJobs;
+if ($options =~ s/-j\s*([0-9]+)//) {
+    $numJobs = $1;
+} else {
+    chomp($numJobs = `num-cpus`);
+}
+
+print STDERR "\n\n####### COMPILING $numSources FILES USING AT MOST $numJobs PARALLEL INSTANCES OF cl.exe ###########\n\n";# if $debug;
+
+# Magic determination of job size
+# The hope is that by splitting the source files up into 2*$numJobs pieces, we
+# won't suffer too much if one job finishes much more quickly than another.
+# However, we don't want to split it up too much due to cl.exe overhead, so set
+# the minimum job size to 5.
+my $jobSize = POSIX::ceil($numSources / (2 * $numJobs));
+$jobSize = $jobSize < 5 ? 5 : $jobSize;
+
+print STDERR "######## jobSize = $jobSize ##########\n" if $debug;
+
+# Sort the source files randomly so that we don't end up with big clumps of large files (aka SVG)
+sub fisher_yates_shuffle(\@)
+{
+    my ($array) = @_;
+    for (my $i = @{$array}; --$i; ) {
+        my $j = int(rand($i+1));
+        next if $i == $j;
+        @{$array}[$i,$j] = @{$array}[$j,$i];
+    }
+}
+
+fisher_yates_shuffle(@sources);    # permutes @array in place
+
+my @children;
+my @tmpFiles;
+my $status = 0;
+while (@sources) {
+    while (@sources && @children < $numJobs) {
+        my $pid;
+        my $tmpFile;
+        my $job = makeJob(@sources, $jobSize);
+        ($pid, $tmpFile) = forkAndCompileFiles(@{$job}, $options);
+
+        print STDERR "####### Spawned child with PID $pid and tmpFile $tmpFile ##########\n" if $debug;
+        push(@children, $pid);
+        push(@tmpFiles, $tmpFile);
+    }
+
+    $status |= waitForChild(@children);
+}
+
+while (@children) {
+    $status |= waitForChild(@children);
+}
+cleanup(@tmpFiles);
+
+exit WEXITSTATUS($status);
+
+
+sub makeJob(\@$)
+{
+    my ($files, $jobSize) = @_;
+
+    my @job;
+    if (@{$files} > ($jobSize * 1.5)) {
+        @job = splice(@{$files}, -$jobSize);
+    } else {
+        # Compile all the remaining files in this job to avoid having a small job later
+        @job = splice(@{$files});
+    }
+
+    return \@job;
+}
+
+sub forkAndCompileFiles(\@$)
+{
+    print STDERR "######## forkAndCompileFiles()\n" if $debug;
+    my ($files, $options) = @_;
+
+    if ($debug) {
+        foreach my $file (@{$files}) {
+            print STDERR "######## $file\n";
+        }
+    }
+
+    my (undef, $tmpFile) = File::Temp::tempfile('clcommandXXXXX', DIR => File::Spec->tmpdir, OPEN => 0);
+
+    my $pid = fork();
+    die "Fork failed" unless defined($pid);
+
+    unless ($pid) {
+        # Child process
+        open(TMP, '>:raw:encoding(UTF16-LE):crlf:utf8', $tmpFile) or die "Couldn't open $tmpFile";
+        print TMP "$options\n";
+        foreach my $file (@{$files}) {
+            print TMP "$file\n";
+        }
+        close(TMP);
+        
+        chomp(my $winTmpFile = `cygpath -m $tmpFile`);
+        Exec "\"$clexe\" \@\"$winTmpFile\"";
+    } else {
+        return ($pid, $tmpFile);
+    }
+}
+
+sub Exec($)
+{
+    my ($command) = @_;
+
+    print STDERR "Exec($command)\n" if $debug;
+
+    exec($command);
+}
+
+sub waitForChild(\@)
+{
+    my ($children) = @_;
+
+    return unless @{$children};
+
+    my $deceased = wait();
+    my $status = $?;
+    print STDERR "######## Child with PID $deceased finished ###########\n" if $debug;
+    for (my $i = 0; $i < @{$children}; $i++) {
+        if ($children->[$i] == $deceased) {
+            splice(@{$children}, $i, 1);
+            last;
+        }
+    }
+
+    return $status;
+}
+
+sub cleanup(\@)
+{
+    my ($tmpFiles) = @_;
+
+    foreach my $file (@{$tmpFiles}) {
+        unlink $file;
+    }
+}
diff --git a/Tools/Scripts/parse-malloc-history b/Tools/Scripts/parse-malloc-history
new file mode 100755
index 0000000..375203f
--- /dev/null
+++ b/Tools/Scripts/parse-malloc-history
@@ -0,0 +1,177 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2007 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.
+
+# Parses the callstacks in a file with malloc_history formatted content, sorting
+# based on total number of bytes allocated, and filtering based on command-line
+# parameters.
+
+use Getopt::Long;
+use File::Basename;
+
+use strict;
+use warnings;
+
+sub commify($);
+
+sub main()
+{
+    my $usage =
+        "Usage: " . basename($0) . " [options] malloc_history.txt\n" .
+        "  --grep-regexp        Include only call stacks that match this regular expression.\n" .
+        "  --byte-minimum       Include only call stacks with allocation sizes >= this value.\n" .
+        "  --merge-regexp       Merge all call stacks that match this regular expression.\n" .
+        "  --merge-depth        Merge all call stacks that match at this stack depth and above.\n";
+
+    my $grepRegexp = "";
+    my $byteMinimum = "";
+    my @mergeRegexps = ();
+    my $mergeDepth = "";
+    my $getOptionsResult = GetOptions(
+        "grep-regexp:s" => \$grepRegexp,
+        "byte-minimum:i" => \$byteMinimum,
+        "merge-regexp:s" => \@mergeRegexps,
+        "merge-depth:i" => \$mergeDepth
+    );
+    die $usage if (!$getOptionsResult || !scalar(@ARGV));
+
+    my @lines = ();
+    foreach my $fileName (@ARGV) {
+        open FILE, "<$fileName" or die "bad file: $fileName";
+        push(@lines, <FILE>);
+        close FILE;
+    }
+
+    my %callstacks = ();
+    my $byteCountTotal = 0;
+
+    for (my $i = 0; $i < @lines; $i++) {
+        my $line = $lines[$i];
+        my ($callCount, $byteCount);
+
+        # First try malloc_history format
+        #   6 calls for 664 bytes thread_ffffffff |0x0 | start
+        ($callCount, $byteCount) = ($line =~ /(\d+) calls for (\d+) bytes/);
+        
+        # Then try leaks format
+        #   Leak: 0x0ac3ca40  size=48
+        #   0x00020001 0x00000001 0x00000000 0x00000000     ................
+        #   Call stack: [thread ffffffff]: | 0x0 | start
+        if (!$callCount || !$byteCount) {
+            $callCount = 1;
+            ($byteCount) = ($line =~ /Leak: [x[:xdigit:]]*  size=(\d+)/);
+
+            if ($byteCount) {
+                while (!($line =~ "Call stack: ")) {
+                    $i++;
+                    $line = $lines[$i];
+                }
+            }
+        }
+        
+        # Then try LeakFinder format
+        # --------------- Key: 213813, 84 bytes ---------
+        # c:\cygwin\home\buildbot\webkit\opensource\webcore\rendering\renderarena.cpp(78): WebCore::RenderArena::allocate
+        # c:\cygwin\home\buildbot\webkit\opensource\webcore\rendering\renderobject.cpp(82): WebCore::RenderObject::operator new
+        if (!$callCount || !$byteCount) {
+            $callCount = 1;
+            ($byteCount) = ($line =~ /Key: (?:\d+), (\d+) bytes/);
+            if ($byteCount) {
+                $line = $lines[++$i];
+                my @tempStack;
+                while ($lines[$i+1] !~ /^(?:-|\d)/) {
+                    if ($line =~ /\): (.*)$/) {
+                        my $call = $1;
+                        $call =~ s/\r$//;
+                        unshift(@tempStack, $call);
+                    }
+                    $line = $lines[++$i];
+                }            
+                $line = join(" | ", @tempStack);
+            }
+        }
+        
+        # Then give up
+        next if (!$callCount || !$byteCount);
+        
+        $byteCountTotal += $byteCount;
+
+        next if ($grepRegexp && !($line =~ $grepRegexp));
+
+        my $callstackBegin = 0;
+        if ($mergeDepth) {
+            # count stack frames backwards from end of callstack
+            $callstackBegin = length($line);
+            for (my $pipeCount = 0; $pipeCount < $mergeDepth; $pipeCount++) {
+                my $rindexResult = rindex($line, "|", $callstackBegin - 1);
+                last if $rindexResult == -1;
+                $callstackBegin = $rindexResult;
+            }
+        } else {
+            # start at beginning of callstack
+            $callstackBegin = index($line, "|");
+        }
+
+        my $callstack = substr($line, $callstackBegin + 2); # + 2 skips "| "
+        for my $regexp (@mergeRegexps) {
+            if ($callstack =~ $regexp) {
+                $callstack = $regexp . "\n";
+                last;
+            }
+        }
+        
+        if (!$callstacks{$callstack}) {
+            $callstacks{$callstack} = {"callCount" => 0, "byteCount" => 0};
+        }
+
+        $callstacks{$callstack}{"callCount"} += $callCount;
+        $callstacks{$callstack}{"byteCount"} += $byteCount;
+    }
+
+    my $byteCountTotalReported = 0;
+    for my $callstack (sort { $callstacks{$b}{"byteCount"} <=> $callstacks{$a}{"byteCount"} } keys %callstacks) {
+        my $callCount = $callstacks{$callstack}{"callCount"};
+        my $byteCount = $callstacks{$callstack}{"byteCount"};
+        last if ($byteMinimum && $byteCount < $byteMinimum);
+
+        $byteCountTotalReported += $byteCount;
+        print commify($callCount) . " calls for " . commify($byteCount) . " bytes: $callstack\n";
+    }
+
+    print "total: " . commify($byteCountTotalReported) . " bytes (" . commify($byteCountTotal - $byteCountTotalReported) . " bytes excluded).\n";
+    return 0;
+}
+
+exit(main());
+
+# Copied from perldoc -- please excuse the style
+sub commify($)
+{
+    local $_  = shift;
+    1 while s/^([-+]?\d+)(\d{3})/$1,$2/;
+    return $_;
+}
diff --git a/Tools/Scripts/pdevenv b/Tools/Scripts/pdevenv
new file mode 100755
index 0000000..4643728
--- /dev/null
+++ b/Tools/Scripts/pdevenv
@@ -0,0 +1,45 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use File::Temp qw/tempfile/;
+use FindBin;
+
+use lib $FindBin::Bin;
+use webkitdirs;
+
+my ($fh, $path) = tempfile(UNLINK => 0, SUFFIX => '.cmd') or die;
+
+chomp(my $vcBin = `cygpath -w "$FindBin::Bin/../vcbin"`);
+chomp(my $scriptsPath = `cygpath -w "$FindBin::Bin"`);
+
+my $vsToolsVar;
+if ($ENV{'VS80COMNTOOLS'}) {
+    $vsToolsVar = "VS80COMNTOOLS";
+} elsif ($ENV{'VS90COMNTOOLS'}) {
+    $vsToolsVar = "VS90COMNTOOLS";
+} else {
+    print "*************************************************************\n";
+    print "Cannot find Visual Studio tools dir.\n";
+    print "Please ensure that \$VS80COMNTOOLS or \$VS90COMNTOOLS\n";
+    print "is set to a valid location.\n";
+    print "*************************************************************\n";
+    die;
+}
+
+print $fh "\@echo off\n\n";
+print $fh "call \"\%" . $vsToolsVar . "\%\\vsvars32.bat\"\n\n";
+print $fh "set PATH=$vcBin;$scriptsPath;\%PATH\%\n\n";
+
+print $fh "IF EXIST \"\%VSINSTALLDIR\%\\Common7\\IDE\\devenv.com\" (devenv.com /useenv " . join(" ", @ARGV) . ") ELSE ";
+print $fh "VCExpress.exe /useenv " . join(" ", @ARGV) . "\n";
+
+
+close $fh;
+
+chmod 0755, $path;
+
+chomp($path = `cygpath -w -s '$path'`);
+
+exec("cmd /c \"call $path\"");
diff --git a/Tools/Scripts/prepare-ChangeLog b/Tools/Scripts/prepare-ChangeLog
new file mode 100755
index 0000000..a3816e2
--- /dev/null
+++ b/Tools/Scripts/prepare-ChangeLog
@@ -0,0 +1,1993 @@
+#!/usr/bin/perl -w
+# -*- Mode: perl; indent-tabs-mode: nil; c-basic-offset: 2  -*-
+
+#
+#  Copyright (C) 2000, 2001 Eazel, Inc.
+#  Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007 Apple Inc.  All rights reserved.
+#  Copyright (C) 2009 Torch Mobile, Inc.
+#  Copyright (C) 2009 Cameron McCormack <cam@mcc.id.au>
+#
+#  prepare-ChangeLog is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public
+#  License as published by the Free Software Foundation; either
+#  version 2 of the License, or (at your option) any later version.
+#
+#  prepare-ChangeLog is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public
+#  License along with this program; if not, write to the Free
+#  Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+
+
+# Perl script to create a ChangeLog entry with names of files
+# and functions from a diff.
+#
+# Darin Adler <darin@bentspoon.com>, started 20 April 2000
+# Java support added by Maciej Stachowiak <mjs@eazel.com>
+# Objective-C, C++ and Objective-C++ support added by Maciej Stachowiak <mjs@apple.com>
+# Git support added by Adam Roben <aroben@apple.com>
+# --git-index flag added by Joe Mason <joe.mason@torchmobile.com>
+
+
+#
+# TODO:
+#   List functions that have been removed too.
+#   Decide what a good logical order is for the changed files
+#     other than a normal text "sort" (top level first?)
+#     (group directories?) (.h before .c?)
+#   Handle yacc source files too (other languages?).
+#   Help merge when there are ChangeLog conflicts or if there's
+#     already a partly written ChangeLog entry.
+#   Add command line option to put the ChangeLog into a separate file.
+#   Add SVN version numbers for commit (can't do that until
+#     the changes are checked in, though).
+#   Work around diff stupidity where deleting a function that starts
+#     with a comment makes diff think that the following function
+#     has been changed (if the following function starts with a comment
+#     with the same first line, such as /**)
+#   Work around diff stupidity where deleting an entire function and
+#     the blank lines before it makes diff think you've changed the
+#     previous function.
+
+use strict;
+use warnings;
+
+use File::Basename;
+use File::Spec;
+use FindBin;
+use Getopt::Long;
+use lib $FindBin::Bin;
+use POSIX qw(strftime);
+use VCSUtils;
+
+sub changeLogDate($);
+sub changeLogEmailAddressFromArgs($$);
+sub changeLogNameFromArgs($$);
+sub createPatchCommand($$$$);
+sub decodeEntities($);
+sub determinePropertyChanges($$$);
+sub diffCommand($$$$);
+sub diffFromToString($$$);
+sub diffHeaderFormat();
+sub extractLineRange($);
+sub fetchBugDescriptionFromURL($$);
+sub findChangeLogs($);
+sub findOriginalFileFromSvn($);
+sub generateFileList(\%$$$);
+sub generateFunctionLists($$$$$);
+sub generateNewChangeLogs($$$$$$$$$$$);
+sub getLatestChangeLogs($);
+sub get_function_line_ranges($$);
+sub get_function_line_ranges_for_cpp($$);
+sub get_function_line_ranges_for_java($$);
+sub get_function_line_ranges_for_javascript($$);
+sub get_function_line_ranges_for_perl($$);
+sub get_selector_line_ranges_for_css($$);
+sub isAddedStatus($);
+sub isConflictStatus($$$);
+sub isModifiedStatus($);
+sub isUnmodifiedStatus($);
+sub main();
+sub method_decl_to_selector($);
+sub normalizeLineEndings($$);
+sub openChangeLogs($);
+sub pluralizeAndList($$@);
+sub printDiff($$$$);
+sub processPaths(\@);
+sub propertyChangeDescription($);
+sub resolveConflictedChangeLogs($);
+sub reviewerAndDescriptionForGitCommit($$);
+sub statusCommand($$$$);
+sub statusDescription($$$$);
+sub testListForChangeLog(@);
+
+### Constant variables.
+# Project time zone for Cupertino, CA, US
+use constant ChangeLogTimeZone => "PST8PDT";
+use constant SVN => "svn";
+use constant GIT => "git";
+use constant SupportedTestExtensions => {map { $_ => 1 } qw(html shtml svg xml xhtml pl php)};
+
+exit(main());
+
+sub main()
+{
+    my $bugDescription;
+    my $bugNumber;
+    my $name;
+    my $emailAddress;
+    my $mergeBase = 0;
+    my $gitCommit = 0;
+    my $gitIndex = "";
+    my $gitReviewer = "";
+    my $openChangeLogs = 0;
+    my $writeChangeLogs = 1;
+    my $showHelp = 0;
+    my $spewDiff = $ENV{"PREPARE_CHANGELOG_DIFF"};
+    my $updateChangeLogs = 1;
+    my $parseOptionsResult =
+        GetOptions("diff|d!" => \$spewDiff,
+                   "bug|b:i" => \$bugNumber,
+                   "description:s" => \$bugDescription,
+                   "name:s" => \$name,
+                   "email:s" => \$emailAddress,
+                   "merge-base:s" => \$mergeBase,
+                   "git-commit|g:s" => \$gitCommit,
+                   "git-index" => \$gitIndex,
+                   "git-reviewer:s" => \$gitReviewer,
+                   "help|h!" => \$showHelp,
+                   "open|o!" => \$openChangeLogs,
+                   "write!" => \$writeChangeLogs,
+                   "update!" => \$updateChangeLogs);
+    if (!$parseOptionsResult || $showHelp) {
+        print STDERR basename($0) . " [-b|--bug=<bugid>] [-d|--diff] [-h|--help] [-o|--open] [-g|--git-commit=<committish>] [--git-reviewer=<name>] [svndir1 [svndir2 ...]]\n";
+        print STDERR "  -b|--bug        Fill in the ChangeLog bug information from the given bug.\n";
+        print STDERR "  --description   One-line description that matches the bug title.\n";
+        print STDERR "  -d|--diff       Spew diff to stdout when running\n";
+        print STDERR "  --merge-base    Populate the ChangeLogs with the diff to this branch\n";
+        print STDERR "  -g|--git-commit Populate the ChangeLogs from the specified git commit\n";
+        print STDERR "  --git-index     Populate the ChangeLogs from the git index only\n";
+        print STDERR "  --git-reviewer  When populating the ChangeLogs from a git commit claim that the spcified name reviewed the change.\n";
+        print STDERR "                  This option is useful when the git commit lacks a Signed-Off-By: line\n";
+        print STDERR "  -h|--help       Show this help message\n";
+        print STDERR "  -o|--open       Open ChangeLogs in an editor when done\n";
+        print STDERR "  --[no-]update   Update ChangeLogs from svn before adding entry (default: update)\n";
+        print STDERR "  --[no-]write    Write ChangeLogs to disk (otherwise send new entries to stdout) (default: write)\n";
+        print STDERR "  --email=        Specify the email address to be used in the patch\n";
+        return 1;
+    }
+
+    die "--git-commit and --git-index are incompatible." if ($gitIndex && $gitCommit);
+
+    isSVN() || isGit() || die "Couldn't determine your version control system.";
+
+    my %paths = processPaths(@ARGV);
+
+    # Find the list of modified files
+    my ($changedFiles, $conflictFiles, $functionLists, $addedRegressionTests) = generateFileList(%paths, $gitCommit, $gitIndex, $mergeBase);
+
+    if (!@$changedFiles && !@$conflictFiles && !keys %$functionLists) {
+        print STDERR "  No changes found.\n";
+        return 1;
+    }
+
+    if (@$conflictFiles) {
+        print STDERR "  The following files have conflicts. Run prepare-ChangeLog again after fixing the conflicts:\n";
+        print STDERR join("\n", @$conflictFiles), "\n";
+        return 1;
+    }
+
+    generateFunctionLists($changedFiles, $functionLists, $gitCommit, $gitIndex, $mergeBase);
+
+    # Get some parameters for the ChangeLog we are about to write.
+    $name = changeLogNameFromArgs($name, $gitCommit);
+    $emailAddress = changeLogEmailAddressFromArgs($emailAddress, $gitCommit);
+
+    print STDERR "  Change author: $name <$emailAddress>.\n";
+
+    # Remove trailing parenthesized notes from user name (bit of hack).
+    $name =~ s/\(.*?\)\s*$//g;
+
+    my $bugURL;
+    if ($bugNumber) {
+        $bugURL = "https://bugs.webkit.org/show_bug.cgi?id=$bugNumber";
+    }
+
+    if ($bugNumber && !$bugDescription) {
+        $bugDescription = fetchBugDescriptionFromURL($bugURL, $bugNumber);
+    }
+
+    my ($filesInChangeLog, $prefixes) = findChangeLogs($functionLists);
+
+    # Get the latest ChangeLog files from svn.
+    my $changeLogs = getLatestChangeLogs($prefixes);
+
+    if (@$changeLogs && $updateChangeLogs && isSVN()) {
+        resolveConflictedChangeLogs($changeLogs);
+    }
+
+    generateNewChangeLogs($prefixes, $filesInChangeLog, $addedRegressionTests, $functionLists, $bugURL, $bugDescription, $name, $emailAddress, $gitReviewer, $gitCommit, $writeChangeLogs);
+
+    if ($writeChangeLogs) {
+        print STDERR "-- Please remember to include a detailed description in your ChangeLog entry. --\n-- See <http://webkit.org/coding/contributing.html> for more info --\n";
+    }
+
+    # Write out another diff.
+    if ($spewDiff && @$changedFiles) {
+        printDiff($changedFiles, $gitCommit, $gitIndex, $mergeBase);
+    }
+
+    # Open ChangeLogs.
+    if ($openChangeLogs && @$changeLogs) {
+        openChangeLogs($changeLogs);
+    }
+    return 0;
+}
+
+sub generateFunctionLists($$$$$)
+{
+    my ($changedFiles, $functionLists, $gitCommit, $gitIndex, $mergeBase) = @_;
+
+    my %changed_line_ranges;
+    if (@$changedFiles) {
+        # For each file, build a list of modified lines.
+        # Use line numbers from the "after" side of each diff.
+        print STDERR "  Reviewing diff to determine which lines changed.\n";
+        my $file;
+        open DIFF, "-|", diffCommand($changedFiles, $gitCommit, $gitIndex, $mergeBase) or die "The diff failed: $!.\n";
+        while (<DIFF>) {
+            $file = makeFilePathRelative($1) if $_ =~ diffHeaderFormat();
+            if (defined $file) {
+                my ($start, $end) = extractLineRange($_);
+                if ($start >= 0 && $end >= 0) {
+                    push @{$changed_line_ranges{$file}}, [ $start, $end ];
+                } elsif (/DO_NOT_COMMIT/) {
+                    print STDERR "WARNING: file $file contains the string DO_NOT_COMMIT, line $.\n";
+                }
+            }
+        }
+        close DIFF;
+    }
+
+    # For each source file, convert line range to function list.
+    if (%changed_line_ranges) {
+        print STDERR "  Extracting affected function names from source files.\n";
+        foreach my $file (keys %changed_line_ranges) {
+            # Find all the functions in the file.
+            open SOURCE, $file or next;
+            my @function_ranges = get_function_line_ranges(\*SOURCE, $file);
+            close SOURCE;
+
+            # Find all the modified functions.
+            my @functions;
+            my %saw_function;
+            my @change_ranges = (@{$changed_line_ranges{$file}}, []);
+            my @change_range = (0, 0);
+            FUNCTION: foreach my $function_range_ref (@function_ranges) {
+                my @function_range = @$function_range_ref;
+
+                # FIXME: This is a hack. If the function name is empty, skip it.
+                # The cpp, python, javascript, perl, css and java parsers
+                # are not perfectly implemented and sometimes function names cannot be retrieved
+                # correctly. As you can see in get_function_line_ranges_XXXX(), those parsers
+                # are not intended to implement real parsers but intended to just retrieve function names
+                # for most practical syntaxes.
+                next unless $function_range[2];
+
+                # Advance to successive change ranges.
+                for (;; @change_range = @{shift @change_ranges}) {
+                    last FUNCTION unless @change_range;
+
+                    # If past this function, move on to the next one.
+                    next FUNCTION if $change_range[0] > $function_range[1];
+
+                    # If an overlap with this function range, record the function name.
+                    if ($change_range[1] >= $function_range[0]
+                        and $change_range[0] <= $function_range[1]) {
+                        if (!$saw_function{$function_range[2]}) {
+                            $saw_function{$function_range[2]} = 1;
+                            push @functions, $function_range[2];
+                        }
+                        next FUNCTION;
+                    }
+                }
+            }
+
+            # Format the list of functions now.
+            if (@functions) {
+                $functionLists->{$file} = "" if !defined $functionLists->{$file};
+                $functionLists->{$file} .= "\n        (" . join("):\n        (", @functions) . "):";
+            }
+        }
+    }
+}
+
+sub changeLogDate($)
+{
+    my ($timeZone) = @_;
+    my $savedTimeZone = $ENV{'TZ'};
+    # Set TZ temporarily so that localtime() is in that time zone
+    $ENV{'TZ'} = $timeZone;
+    my $date = strftime("%Y-%m-%d", localtime());
+    if (defined $savedTimeZone) {
+         $ENV{'TZ'} = $savedTimeZone;
+    } else {
+         delete $ENV{'TZ'};
+    }
+    return $date;
+}
+
+sub changeLogNameFromArgs($$)
+{
+    my ($nameFromArgs, $gitCommit) = @_;
+    # Silently allow --git-commit to win, we could warn if $nameFromArgs is defined.
+    my $command = GIT . ' log --max-count=1 --pretty="format:%an" "' . $gitCommit . '"';
+    return `$command` if $gitCommit;
+
+    return $nameFromArgs || changeLogName();
+}
+
+sub changeLogEmailAddressFromArgs($$)
+{
+    my ($emailAddressFromArgs, $gitCommit) = @_;
+    # Silently allow --git-commit to win, we could warn if $emailAddressFromArgs is defined.
+    my $command = GIT . ' log --max-count=1 --pretty="format:%ae" "' . $gitCommit . '"';
+    return `$command` if $gitCommit;
+
+    return $emailAddressFromArgs || changeLogEmailAddress();
+}
+
+sub fetchBugDescriptionFromURL($$)
+{
+    my ($bugURL, $bugNumber) = @_;
+
+    my $bugXMLURL = "$bugURL&ctype=xml&excludefield=attachmentdata";
+    # Perl has no built in XML processing, so we'll fetch and parse with curl and grep
+    # Pass --insecure because some cygwin installs have no certs we don't
+    # care about validating that bugs.webkit.org is who it says it is here.
+    my $descriptionLine = `curl --insecure --silent "$bugXMLURL" | grep short_desc`;
+    if ($descriptionLine !~ /<short_desc>(.*)<\/short_desc>/) {
+        # Maybe the reason the above did not work is because the curl that is installed doesn't
+        # support ssl at all.
+        if (`curl --version | grep ^Protocols` !~ /\bhttps\b/) {
+            print STDERR "  Could not get description for bug $bugNumber.\n";
+            print STDERR "  It looks like your version of curl does not support ssl.\n";
+            print STDERR "  If you are using macports, this can be fixed with sudo port install curl +ssl.\n";
+        } else {
+            print STDERR "  Bug $bugNumber has no bug description. Maybe you set wrong bug ID?\n";
+            print STDERR "  The bug URL: $bugXMLURL\n";
+        }
+        exit 1;
+    }
+    my $bugDescription = decodeEntities($1);
+    print STDERR "  Description from bug $bugNumber:\n    \"$bugDescription\".\n";
+    return $bugDescription;
+}
+
+sub findChangeLogs($)
+{
+    my ($functionLists) = @_;
+
+    # Find the change logs.
+    my %has_log;
+    my %filesInChangeLog;
+    foreach my $file (sort keys %$functionLists) {
+        my $prefix = $file;
+        my $has_log = 0;
+        while ($prefix) {
+            $prefix =~ s-/[^/]+/?$-/- or $prefix = "";
+            $has_log = $has_log{$prefix};
+            if (!defined $has_log) {
+                $has_log = -f "${prefix}ChangeLog";
+                $has_log{$prefix} = $has_log;
+            }
+            last if $has_log;
+        }
+        if (!$has_log) {
+            print STDERR "No ChangeLog found for $file.\n";
+        } else {
+            push @{$filesInChangeLog{$prefix}}, $file;
+        }
+    }
+
+    # Build the list of ChangeLog prefixes in the correct project order
+    my @prefixes;
+    my %prefixesSort;
+    foreach my $prefix (keys %filesInChangeLog) {
+        my $prefixDir = substr($prefix, 0, length($prefix) - 1); # strip trailing /
+        my $sortKey = lc $prefix;
+        $sortKey = "top level" unless length $sortKey;
+
+        if ($prefixDir eq "top level") {
+            $sortKey = "";
+        } elsif ($prefixDir eq "Tools") {
+            $sortKey = "-, just after top level";
+        } elsif ($prefixDir eq "WebBrowser") {
+            $sortKey = lc "WebKit, WebBrowser after";
+        } elsif ($prefixDir eq "Source/WebCore") {
+            $sortKey = lc "WebFoundation, WebCore after";
+        } elsif ($prefixDir eq "LayoutTests") {
+            $sortKey = lc "~, LayoutTests last";
+        }
+
+        $prefixesSort{$sortKey} = $prefix;
+    }
+    foreach my $prefixSort (sort keys %prefixesSort) {
+        push @prefixes, $prefixesSort{$prefixSort};
+    }
+    return (\%filesInChangeLog, \@prefixes);
+}
+
+sub getLatestChangeLogs($)
+{
+    my ($prefixes) = @_;
+
+    my @changeLogs = ();
+    foreach my $prefix (@$prefixes) {
+        push @changeLogs, File::Spec->catfile($prefix || ".", changeLogFileName());
+    }
+    return \@changeLogs;
+}
+
+sub resolveConflictedChangeLogs($)
+{
+    my ($changeLogs) = @_;
+
+    print STDERR "  Running 'svn update' to update ChangeLog files.\n";
+    open ERRORS, "-|", SVN, "update", @$changeLogs
+        or die "The svn update of ChangeLog files failed: $!.\n";
+    my @conflictedChangeLogs;
+    while (my $line = <ERRORS>) {
+        print STDERR "    ", $line;
+        push @conflictedChangeLogs, $1 if $line =~ m/^C\s+(.+?)[\r\n]*$/;
+    }
+    close ERRORS;
+
+    return if !@conflictedChangeLogs;
+
+    print STDERR "  Attempting to merge conflicted ChangeLogs.\n";
+    my $resolveChangeLogsPath = File::Spec->catfile(dirname($0), "resolve-ChangeLogs");
+    open RESOLVE, "-|", $resolveChangeLogsPath, "--no-warnings", @conflictedChangeLogs
+        or die "Could not open resolve-ChangeLogs script: $!.\n";
+    print STDERR "    $_" while <RESOLVE>;
+    close RESOLVE;
+}
+
+sub generateNewChangeLogs($$$$$$$$$$$)
+{
+    my ($prefixes, $filesInChangeLog, $addedRegressionTests, $functionLists, $bugURL, $bugDescription, $name, $emailAddress, $gitReviewer, $gitCommit, $writeChangeLogs) = @_;
+
+    # Generate new ChangeLog entries and (optionally) write out new ChangeLog files.
+    foreach my $prefix (@$prefixes) {
+        my $endl = "\n";
+        my @old_change_log;
+
+        if ($writeChangeLogs) {
+            my $changeLogPath = File::Spec->catfile($prefix || ".", changeLogFileName());
+            print STDERR "  Editing the ${changeLogPath} file.\n";
+            open OLD_CHANGE_LOG, ${changeLogPath} or die "Could not open ${changeLogPath} file: $!.\n";
+            # It's less efficient to read the whole thing into memory than it would be
+            # to read it while we prepend to it later, but I like doing this part first.
+            @old_change_log = <OLD_CHANGE_LOG>;
+            close OLD_CHANGE_LOG;
+            # We want to match the ChangeLog's line endings in case it doesn't match
+            # the native line endings for this version of perl.
+            if ($old_change_log[0] =~ /(\r?\n)$/g) {
+                $endl = "$1";
+            }
+            open CHANGE_LOG, "> ${changeLogPath}" or die "Could not write ${changeLogPath}\n.";
+        } else {
+            open CHANGE_LOG, ">-" or die "Could not write to STDOUT\n.";
+            print substr($prefix, 0, length($prefix) - 1) . ":\n\n" unless (scalar @$prefixes) == 1;
+        }
+
+        my $date = changeLogDate(ChangeLogTimeZone);
+        print CHANGE_LOG normalizeLineEndings("$date  $name  <$emailAddress>\n\n", $endl);
+
+        my ($reviewer, $description) = reviewerAndDescriptionForGitCommit($gitCommit, $gitReviewer) if $gitCommit;
+        $reviewer = "NOBODY (OO" . "PS!)" if !$reviewer;
+
+        print CHANGE_LOG normalizeLineEndings($description . "\n", $endl) if $description;
+
+        $bugDescription = "Need a short description (OOPS!).\n        Need the bug URL (OOPS!)." unless $bugDescription;
+        print CHANGE_LOG normalizeLineEndings("        $bugDescription\n", $endl) if $bugDescription;
+        print CHANGE_LOG normalizeLineEndings("        $bugURL\n", $endl) if $bugURL;
+        print CHANGE_LOG normalizeLineEndings("\n", $endl);
+
+        print CHANGE_LOG normalizeLineEndings("        Reviewed by $reviewer.\n\n", $endl);
+        print CHANGE_LOG normalizeLineEndings("        Additional information of the change such as approach, rationale. Please add per-function descriptions below (OOPS!).\n\n", $endl);
+
+        if ($prefix =~ m/WebCore/ || `pwd` =~ m/WebCore/) {
+            if (@$addedRegressionTests) {
+                print CHANGE_LOG normalizeLineEndings(testListForChangeLog(sort @$addedRegressionTests), $endl);
+            } else {
+                print CHANGE_LOG normalizeLineEndings("        No new tests (OOPS!).\n\n", $endl);
+            }
+        }
+
+        foreach my $file (sort @{$filesInChangeLog->{$prefix}}) {
+            my $file_stem = substr $file, length $prefix;
+            print CHANGE_LOG normalizeLineEndings("        * $file_stem:$functionLists->{$file}\n", $endl);
+        }
+
+        if ($writeChangeLogs) {
+            print CHANGE_LOG normalizeLineEndings("\n", $endl), @old_change_log;
+        } else {
+            print CHANGE_LOG "\n";
+        }
+
+        close CHANGE_LOG;
+    }
+}
+
+sub printDiff($$$$)
+{
+    my ($changedFiles, $gitCommit, $gitIndex, $mergeBase) = @_;
+
+    print STDERR "  Running diff to help you write the ChangeLog entries.\n";
+    local $/ = undef; # local slurp mode
+    my $changedFilesString = "'" . join("' '", @$changedFiles) . "'";
+    open DIFF, "-|", createPatchCommand($changedFilesString, $gitCommit, $gitIndex, $mergeBase) or die "The diff failed: $!.\n";
+    print <DIFF>;
+    close DIFF;
+}
+
+sub openChangeLogs($)
+{
+    my ($changeLogs) = @_;
+
+    print STDERR "  Opening the edited ChangeLog files.\n";
+    my $editor = $ENV{CHANGE_LOG_EDITOR} || $ENV{VISUAL} || $ENV{EDITOR};
+    if ($editor) {
+        system ((split ' ', $editor), @$changeLogs);
+    } else {
+        $editor = $ENV{CHANGE_LOG_EDIT_APPLICATION};
+        if ($editor) {
+            system "open", "-a", $editor, @$changeLogs;
+        } else {
+            system "open", "-e", @$changeLogs;
+        }
+    }
+}
+
+sub get_function_line_ranges($$)
+{
+    my ($file_handle, $file_name) = @_;
+
+    # Try to determine the source language based on the file extension.
+
+    return get_function_line_ranges_for_cpp($file_handle, $file_name) if $file_name =~ /\.(c|cpp|m|mm|h)$/;
+    return get_function_line_ranges_for_java($file_handle, $file_name) if $file_name =~ /\.java$/;
+    return get_function_line_ranges_for_javascript($file_handle, $file_name) if $file_name =~ /\.js$/;
+    return get_selector_line_ranges_for_css($file_handle, $file_name) if $file_name =~ /\.css$/;
+    return get_function_line_ranges_for_perl($file_handle, $file_name) if $file_name =~ /\.p[lm]$/;
+    return get_function_line_ranges_for_python($file_handle, $file_name) if $file_name =~ /\.py$/ or $file_name =~ /master\.cfg$/;
+
+    # Try to determine the source language based on the script interpreter.
+
+    my $first_line = <$file_handle>;
+    seek($file_handle, 0, 0);
+
+    return () unless $first_line =~ m|^#!(?:/usr/bin/env\s+)?(\S+)|;
+    my $interpreter = $1;
+
+    return get_function_line_ranges_for_perl($file_handle, $file_name) if $interpreter =~ /perl$/;
+    return get_function_line_ranges_for_python($file_handle, $file_name) if $interpreter =~ /python$/;
+
+    return ();
+}
+
+
+sub method_decl_to_selector($)
+{
+    (my $method_decl) = @_;
+
+    $_ = $method_decl;
+
+    if ((my $comment_stripped) = m-([^/]*)(//|/*).*-) {
+        $_ = $comment_stripped;
+    }
+
+    s/,\s*...//;
+
+    if (/:/) {
+        my @components = split /:/;
+        pop @components if (scalar @components > 1);
+        $_ = (join ':', map {s/.*[^[:word:]]//; scalar $_;} @components) . ':';
+    } else {
+        s/\s*$//;
+        s/.*[^[:word:]]//;
+    }
+
+    return $_;
+}
+
+
+
+# Read a file and get all the line ranges of the things that look like C functions.
+# A function name is the last word before an open parenthesis before the outer
+# level open brace. A function starts at the first character after the last close
+# brace or semicolon before the function name and ends at the close brace.
+# Comment handling is simple-minded but will work for all but pathological cases.
+#
+# Result is a list of triples: [ start_line, end_line, function_name ].
+
+sub get_function_line_ranges_for_cpp($$)
+{
+    my ($file_handle, $file_name) = @_;
+
+    my @ranges;
+
+    my $in_comment = 0;
+    my $in_macro = 0;
+    my $in_method_declaration = 0;
+    my $in_parentheses = 0;
+    my $in_braces = 0;
+    my $in_toplevel_array_brace = 0;
+    my $brace_start = 0;
+    my $brace_end = 0;
+    my $namespace_start = -1;
+    my $skip_til_brace_or_semicolon = 0;
+    my $equal_observed = 0;
+
+    my $word = "";
+    my $interface_name = "";
+
+    my $potential_method_char = "";
+    my $potential_method_spec = "";
+
+    my $potential_start = 0;
+    my $potential_name = "";
+
+    my $start = 0;
+    my $name = "";
+
+    my $next_word_could_be_namespace = 0;
+    my $potential_namespace = "";
+    my @namespaces;
+
+    while (<$file_handle>) {
+        # Handle continued multi-line comment.
+        if ($in_comment) {
+            next unless s-.*\*/--;
+            $in_comment = 0;
+        }
+
+        # Handle continued macro.
+        if ($in_macro) {
+            $in_macro = 0 unless /\\$/;
+            next;
+        }
+
+        # Handle start of macro (or any preprocessor directive).
+        if (/^\s*\#/) {
+            $in_macro = 1 if /^([^\\]|\\.)*\\$/;
+            next;
+        }
+
+        # Handle comments and quoted text.
+        while (m-(/\*|//|\'|\")-) { # \' and \" keep emacs perl mode happy
+            my $match = $1;
+            if ($match eq "/*") {
+                if (!s-/\*.*?\*/--) {
+                    s-/\*.*--;
+                    $in_comment = 1;
+                }
+            } elsif ($match eq "//") {
+                s-//.*--;
+            } else { # ' or "
+                if (!s-$match([^\\]|\\.)*?$match--) {
+                    warn "mismatched quotes at line $. in $file_name\n";
+                    s-$match.*--;
+                }
+            }
+        }
+
+
+        # continued method declaration
+        if ($in_method_declaration) {
+              my $original = $_;
+              my $method_cont = $_;
+
+              chomp $method_cont;
+              $method_cont =~ s/[;\{].*//;
+              $potential_method_spec = "${potential_method_spec} ${method_cont}";
+
+              $_ = $original;
+              if (/;/) {
+                  $potential_start = 0;
+                  $potential_method_spec = "";
+                  $potential_method_char = "";
+                  $in_method_declaration = 0;
+                  s/^[^;\{]*//;
+              } elsif (/{/) {
+                  my $selector = method_decl_to_selector ($potential_method_spec);
+                  $potential_name = "${potential_method_char}\[${interface_name} ${selector}\]";
+
+                  $potential_method_spec = "";
+                  $potential_method_char = "";
+                  $in_method_declaration = 0;
+
+                  $_ = $original;
+                  s/^[^;{]*//;
+              } elsif (/\@end/) {
+                  $in_method_declaration = 0;
+                  $interface_name = "";
+                  $_ = $original;
+              } else {
+                  next;
+              }
+        }
+
+
+        # start of method declaration
+        if ((my $method_char, my $method_spec) = m&^([-+])([^0-9;][^;]*);?$&) {
+            my $original = $_;
+
+            if ($interface_name) {
+                chomp $method_spec;
+                $method_spec =~ s/\{.*//;
+
+                $potential_method_char = $method_char;
+                $potential_method_spec = $method_spec;
+                $potential_start = $.;
+                $in_method_declaration = 1;
+            } else { 
+                warn "declaring a method but don't have interface on line $. in $file_name\n";
+            }
+            $_ = $original;
+            if (/\{/) {
+              my $selector = method_decl_to_selector ($potential_method_spec);
+              $potential_name = "${potential_method_char}\[${interface_name} ${selector}\]";
+
+              $potential_method_spec = "";
+              $potential_method_char = "";
+              $in_method_declaration = 0;
+              $_ = $original;
+              s/^[^{]*//;
+            } elsif (/\@end/) {
+              $in_method_declaration = 0;
+              $interface_name = "";
+              $_ = $original;
+            } else {
+              next;
+            }
+        }
+
+
+        # Find function, interface and method names.
+        while (m&((?:[[:word:]]+::)*operator(?:[ \t]*\(\)|[^()]*)|[[:word:]:~]+|[(){}:;=])|\@(?:implementation|interface|protocol)\s+(\w+)[^{]*&g) {
+            # Skip an array definition at the top level.
+            # e.g. static int arr[] = { 1, 2, 3 };
+            if ($1) {
+                if ($1 eq "=" and !$in_parentheses and !$in_braces) {
+                    $equal_observed = 1;
+                } elsif ($1 eq "{" and $equal_observed) {
+                    # This '{' is the beginning of an array definition, not the beginning of a method.
+                    $in_toplevel_array_brace = 1;
+                    $in_braces++;
+                    $equal_observed = 0;
+                    next;
+                } elsif ($1 !~ /[ \t]/) {
+                    $equal_observed = 0;
+                }
+            }
+
+            # interface name
+            if ($2) {
+                $interface_name = $2;
+                next;
+            }
+
+            # Open parenthesis.
+            if ($1 eq "(") {
+                $potential_name = $word unless $in_parentheses || $skip_til_brace_or_semicolon;
+                $in_parentheses++;
+                next;
+            }
+
+            # Close parenthesis.
+            if ($1 eq ")") {
+                $in_parentheses--;
+                next;
+            }
+
+            # C++ constructor initializers
+            if ($1 eq ":") {
+                  $skip_til_brace_or_semicolon = 1 unless ($in_parentheses || $in_braces);
+            }
+
+            # Open brace.
+            if ($1 eq "{") {
+                $skip_til_brace_or_semicolon = 0;
+
+                if (!$in_braces) {
+                    if ($namespace_start >= 0 and $namespace_start < $potential_start) {
+                        push @ranges, [ $namespace_start . "", $potential_start - 1, $name ];
+                    }
+
+                    if ($potential_namespace) {
+                        push @namespaces, $potential_namespace;
+                        $potential_namespace = "";
+                        $name = $namespaces[-1];
+                        $namespace_start = $. + 1;
+                        next;
+                    }
+
+                    # Promote potential name to real function name at the
+                    # start of the outer level set of braces (function body?).
+                    if ($potential_start) {
+                        $start = $potential_start;
+                        $name = $potential_name;
+                        if (@namespaces && $name && (length($name) < 2 || substr($name,1,1) ne "[")) {
+                            $name = join ('::', @namespaces, $name);
+                        }
+                    }
+                }
+
+                $in_method_declaration = 0;
+
+                $brace_start = $. if (!$in_braces);
+                $in_braces++;
+                next;
+            }
+
+            # Close brace.
+            if ($1 eq "}") {
+                if (!$in_braces && @namespaces) {
+                    if ($namespace_start >= 0 and $namespace_start < $.) {
+                        push @ranges, [ $namespace_start . "", $. - 1, $name ];
+                    }
+
+                    pop @namespaces;
+                    if (@namespaces) {
+                        $name = $namespaces[-1];
+                        $namespace_start = $. + 1;
+                    } else {
+                        $name = "";
+                        $namespace_start = -1;
+                    }
+                    next;
+                }
+
+                $in_braces--;
+                $brace_end = $. if (!$in_braces);
+
+                # End of an outer level set of braces.
+                # This could be a function body.
+                if (!$in_braces and $name) {
+                    # This is the end of an array definition at the top level, not the end of a method.
+                    if ($in_toplevel_array_brace) {
+                        $in_toplevel_array_brace = 0;
+                        next;
+                    }
+
+                    push @ranges, [ $start, $., $name ];
+                    if (@namespaces) {
+                        $name = $namespaces[-1];
+                        $namespace_start = $. + 1;
+                    } else {
+                        $name = "";
+                        $namespace_start = -1;
+                    }
+                }
+
+                $potential_start = 0;
+                $potential_name = "";
+                next;
+            }
+
+            # Semicolon.
+            if ($1 eq ";") {
+                $skip_til_brace_or_semicolon = 0;
+                $potential_start = 0;
+                $potential_name = "";
+                $in_method_declaration = 0;
+                next;
+            }
+
+            # Ignore "const" method qualifier.
+            if ($1 eq "const") {
+                next;
+            }
+
+            if ($1 eq "namespace" || $1 eq "class" || $1 eq "struct") {
+                $next_word_could_be_namespace = 1;
+                next;
+            }
+
+            # Word.
+            $word = $1;
+            if (!$skip_til_brace_or_semicolon) {
+                if ($next_word_could_be_namespace) {
+                    $potential_namespace = $word;
+                    $next_word_could_be_namespace = 0;
+                } elsif ($potential_namespace) {
+                    $potential_namespace = "";
+                }
+
+                if (!$in_parentheses) {
+                    $potential_start = 0;
+                    $potential_name = "";
+                }
+                if (!$potential_start) {
+                    $potential_start = $.;
+                    $potential_name = "";
+                }
+            }
+        }
+    }
+
+    warn "missing close braces in $file_name (probable start at $brace_start)\n" if ($in_braces > 0);
+    warn "too many close braces in $file_name (probable start at $brace_end)\n" if ($in_braces < 0);
+
+    warn "mismatched parentheses in $file_name\n" if $in_parentheses;
+
+    return @ranges;
+}
+
+
+
+# Read a file and get all the line ranges of the things that look like Java
+# classes, interfaces and methods.
+#
+# A class or interface name is the word that immediately follows
+# `class' or `interface' when followed by an open curly brace and not
+# a semicolon. It can appear at the top level, or inside another class
+# or interface block, but not inside a function block
+#
+# A class or interface starts at the first character after the first close
+# brace or after the function name and ends at the close brace.
+#
+# A function name is the last word before an open parenthesis before
+# an open brace rather than a semicolon. It can appear at top level or
+# inside a class or interface block, but not inside a function block.
+#
+# A function starts at the first character after the first close
+# brace or after the function name and ends at the close brace.
+#
+# Comment handling is simple-minded but will work for all but pathological cases.
+#
+# Result is a list of triples: [ start_line, end_line, function_name ].
+
+sub get_function_line_ranges_for_java($$)
+{
+    my ($file_handle, $file_name) = @_;
+
+    my @current_scopes;
+
+    my @ranges;
+
+    my $in_comment = 0;
+    my $in_macro = 0;
+    my $in_parentheses = 0;
+    my $in_braces = 0;
+    my $in_non_block_braces = 0;
+    my $class_or_interface_just_seen = 0;
+    my $in_class_declaration = 0;
+
+    my $word = "";
+
+    my $potential_start = 0;
+    my $potential_name = "";
+    my $potential_name_is_class_or_interface = 0;
+
+    my $start = 0;
+    my $name = "";
+    my $current_name_is_class_or_interface = 0;
+
+    while (<$file_handle>) {
+        # Handle continued multi-line comment.
+        if ($in_comment) {
+            next unless s-.*\*/--;
+            $in_comment = 0;
+        }
+
+        # Handle continued macro.
+        if ($in_macro) {
+            $in_macro = 0 unless /\\$/;
+            next;
+        }
+
+        # Handle start of macro (or any preprocessor directive).
+        if (/^\s*\#/) {
+            $in_macro = 1 if /^([^\\]|\\.)*\\$/;
+            next;
+        }
+
+        # Handle comments and quoted text.
+        while (m-(/\*|//|\'|\")-) { # \' and \" keep emacs perl mode happy
+            my $match = $1;
+            if ($match eq "/*") {
+                if (!s-/\*.*?\*/--) {
+                    s-/\*.*--;
+                    $in_comment = 1;
+                }
+            } elsif ($match eq "//") {
+                s-//.*--;
+            } else { # ' or "
+                if (!s-$match([^\\]|\\.)*?$match--) {
+                    warn "mismatched quotes at line $. in $file_name\n";
+                    s-$match.*--;
+                }
+            }
+        }
+
+        # Find function names.
+        while (m-(\w+|[(){};])-g) {
+            # Open parenthesis.
+            if ($1 eq "(") {
+                if (!$in_parentheses) {
+                    $potential_name = $word;
+                    $potential_name_is_class_or_interface = 0;
+                }
+                $in_parentheses++;
+                next;
+            }
+
+            # Close parenthesis.
+            if ($1 eq ")") {
+                $in_parentheses--;
+                next;
+            }
+
+            # Open brace.
+            if ($1 eq "{") {
+                $in_class_declaration = 0;
+
+                # Promote potential name to real function name at the
+                # start of the outer level set of braces (function/class/interface body?).
+                if (!$in_non_block_braces
+                    and (!$in_braces or $current_name_is_class_or_interface)
+                    and $potential_start) {
+                    if ($name) {
+                          push @ranges, [ $start, ($. - 1),
+                                          join ('.', @current_scopes) ];
+                    }
+
+
+                    $current_name_is_class_or_interface = $potential_name_is_class_or_interface;
+
+                    $start = $potential_start;
+                    $name = $potential_name;
+
+                    push (@current_scopes, $name);
+                } else {
+                    $in_non_block_braces++;
+                }
+
+                $potential_name = "";
+                $potential_start = 0;
+
+                $in_braces++;
+                next;
+            }
+
+            # Close brace.
+            if ($1 eq "}") {
+                $in_braces--;
+
+                # End of an outer level set of braces.
+                # This could be a function body.
+                if (!$in_non_block_braces) {
+                    if ($name) {
+                        push @ranges, [ $start, $.,
+                                        join ('.', @current_scopes) ];
+
+                        pop (@current_scopes);
+
+                        if (@current_scopes) {
+                            $current_name_is_class_or_interface = 1;
+
+                            $start = $. + 1;
+                            $name =  $current_scopes[$#current_scopes-1];
+                        } else {
+                            $current_name_is_class_or_interface = 0;
+                            $start = 0;
+                            $name =  "";
+                        }
+                    }
+                } else {
+                    $in_non_block_braces-- if $in_non_block_braces;
+                }
+
+                $potential_start = 0;
+                $potential_name = "";
+                next;
+            }
+
+            # Semicolon.
+            if ($1 eq ";") {
+                $potential_start = 0;
+                $potential_name = "";
+                next;
+            }
+
+            if ($1 eq "class") {
+                $in_class_declaration = 1;
+            }
+            if ($1 eq "class" or (!$in_class_declaration and $1 eq "interface")) {
+                $class_or_interface_just_seen = 1;
+                next;
+            }
+
+            # Word.
+            $word = $1;
+            if (!$in_parentheses) {
+                if ($class_or_interface_just_seen) {
+                    $potential_name = $word;
+                    $potential_start = $.;
+                    $class_or_interface_just_seen = 0;
+                    $potential_name_is_class_or_interface = 1;
+                    next;
+                }
+            }
+            if (!$potential_start) {
+                $potential_start = $.;
+                $potential_name = "";
+            }
+            $class_or_interface_just_seen = 0;
+        }
+    }
+
+    warn "mismatched braces in $file_name\n" if $in_braces;
+    warn "mismatched parentheses in $file_name\n" if $in_parentheses;
+
+    return @ranges;
+}
+
+
+
+# Read a file and get all the line ranges of the things that look like
+# JavaScript functions.
+#
+# A function name is the word that immediately follows `function' when
+# followed by an open curly brace. It can appear at the top level, or
+# inside other functions.
+#
+# An anonymous function name is the identifier chain immediately before
+# an assignment with the equals operator or object notation that has a
+# value starting with `function' followed by an open curly brace.
+#
+# A getter or setter name is the word that immediately follows `get' or
+# `set' when followed by an open curly brace .
+#
+# Comment handling is simple-minded but will work for all but pathological cases.
+#
+# Result is a list of triples: [ start_line, end_line, function_name ].
+
+sub get_function_line_ranges_for_javascript($$)
+{
+    my ($fileHandle, $fileName) = @_;
+
+    my @currentScopes;
+    my @currentIdentifiers;
+    my @currentFunctionNames;
+    my @currentFunctionDepths;
+    my @currentFunctionStartLines;
+
+    my @ranges;
+
+    my $inComment = 0;
+    my $inQuotedText = "";
+    my $parenthesesDepth = 0;
+    my $bracesDepth = 0;
+
+    my $functionJustSeen = 0;
+    my $getterJustSeen = 0;
+    my $setterJustSeen = 0;
+    my $assignmentJustSeen = 0;
+
+    my $word = "";
+
+    while (<$fileHandle>) {
+        # Handle continued multi-line comment.
+        if ($inComment) {
+            next unless s-.*\*/--;
+            $inComment = 0;
+        }
+
+        # Handle continued quoted text.
+        if ($inQuotedText ne "") {
+            next if /\\$/;
+            s-([^\\]|\\.)*?$inQuotedText--;
+            $inQuotedText = "";
+        }
+
+        # Handle comments and quoted text.
+        while (m-(/\*|//|\'|\")-) { # \' and \" keep emacs perl mode happy
+            my $match = $1;
+            if ($match eq '/*') {
+                if (!s-/\*.*?\*/--) {
+                    s-/\*.*--;
+                    $inComment = 1;
+                }
+            } elsif ($match eq '//') {
+                s-//.*--;
+            } else { # ' or "
+                if (!s-$match([^\\]|\\.)*?$match-string_appeared_here-) {
+                    $inQuotedText = $match if /\\$/;
+                    warn "mismatched quotes at line $. in $fileName\n" if $inQuotedText eq "";
+                    s-$match.*--;
+                }
+            }
+        }
+
+        # Find function names.
+        while (m-(\w+|[(){}=:;,])-g) {
+            # Open parenthesis.
+            if ($1 eq '(') {
+                $parenthesesDepth++;
+                next;
+            }
+
+            # Close parenthesis.
+            if ($1 eq ')') {
+                $parenthesesDepth--;
+                next;
+            }
+
+            # Open brace.
+            if ($1 eq '{') {
+                push(@currentScopes, join(".", @currentIdentifiers));
+                @currentIdentifiers = ();
+
+                $bracesDepth++;
+                next;
+            }
+
+            # Close brace.
+            if ($1 eq '}') {
+                $bracesDepth--;
+
+                if (@currentFunctionDepths and $bracesDepth == $currentFunctionDepths[$#currentFunctionDepths]) {
+                    pop(@currentFunctionDepths);
+
+                    my $currentFunction = pop(@currentFunctionNames);
+                    my $start = pop(@currentFunctionStartLines);
+
+                    push(@ranges, [$start, $., $currentFunction]);
+                }
+
+                pop(@currentScopes);
+                @currentIdentifiers = ();
+
+                next;
+            }
+
+            # Semicolon or comma.
+            if ($1 eq ';' or $1 eq ',') {
+                @currentIdentifiers = ();
+                next;
+            }
+
+            # Function.
+            if ($1 eq 'function') {
+                $functionJustSeen = 1;
+
+                if ($assignmentJustSeen) {
+                    my $currentFunction = join('.', (@currentScopes, @currentIdentifiers));
+                    $currentFunction =~ s/\.{2,}/\./g; # Removes consecutive periods.
+
+                    push(@currentFunctionNames, $currentFunction);
+                    push(@currentFunctionDepths, $bracesDepth);
+                    push(@currentFunctionStartLines, $.);
+                }
+
+                next;
+            }
+
+            # Getter prefix.
+            if ($1 eq 'get') {
+                $getterJustSeen = 1;
+                next;
+            }
+
+            # Setter prefix.
+            if ($1 eq 'set') {
+                $setterJustSeen = 1;
+                next;
+            }
+
+            # Assignment operator.
+            if ($1 eq '=' or $1 eq ':') {
+                $assignmentJustSeen = 1;
+                next;
+            }
+
+            next if $parenthesesDepth;
+
+            # Word.
+            $word = $1;
+            $word = "get $word" if $getterJustSeen;
+            $word = "set $word" if $setterJustSeen;
+
+            if (($functionJustSeen and !$assignmentJustSeen) or $getterJustSeen or $setterJustSeen) {
+                push(@currentIdentifiers, $word);
+
+                my $currentFunction = join('.', (@currentScopes, @currentIdentifiers));
+                $currentFunction =~ s/\.{2,}/\./g; # Removes consecutive periods.
+
+                push(@currentFunctionNames, $currentFunction);
+                push(@currentFunctionDepths, $bracesDepth);
+                push(@currentFunctionStartLines, $.);
+            } elsif ($word ne 'if' and $word ne 'for' and $word ne 'do' and $word ne 'while' and $word ne 'which' and $word ne 'var') {
+                push(@currentIdentifiers, $word);
+            }
+
+            $functionJustSeen = 0;
+            $getterJustSeen = 0;
+            $setterJustSeen = 0;
+            $assignmentJustSeen = 0;
+        }
+    }
+
+    warn "mismatched braces in $fileName\n" if $bracesDepth;
+    warn "mismatched parentheses in $fileName\n" if $parenthesesDepth;
+
+    return @ranges;
+}
+
+# Read a file and get all the line ranges of the things that look like Perl functions. Functions
+# start on a line that starts with "sub ", and end on the first line starting with "}" thereafter.
+#
+# Result is a list of triples: [ start_line, end_line, function ].
+
+sub get_function_line_ranges_for_perl($$)
+{
+    my ($fileHandle, $fileName) = @_;
+
+    my @ranges;
+
+    my $currentFunction = "";
+    my $start = 0;
+    my $hereDocumentIdentifier = "";
+
+    while (<$fileHandle>) {
+        chomp;
+        if (!$hereDocumentIdentifier) {
+            if (/^sub\s+([\w_][\w\d_]*)/) {
+                # Skip over forward declarations, which don't contain a brace and end with a semicolon.
+                next if /;\s*$/;
+
+                if ($currentFunction) {
+                    warn "nested functions found at top-level at $fileName:$.\n";
+                    next;
+                }
+                $currentFunction = $1;
+                $start = $.;
+            }
+            if (/<<\s*[\"\']?([\w_][\w_\d]*)/) {
+                # Enter here-document.
+                $hereDocumentIdentifier = $1;
+            }
+            if (index($_, "}") == 0) {
+                next unless $start;
+                push(@ranges, [$start, $., $currentFunction]);
+                $currentFunction = "";
+                $start = 0;
+            }
+        } elsif ($_ eq $hereDocumentIdentifier) {
+            # Escape from here-document.
+            $hereDocumentIdentifier = "";
+        }
+    }
+
+    return @ranges;
+}
+
+# Read a file and get all the line ranges of the things that look like Python classes, methods, or functions.
+#
+# FIXME: Maybe we should use Python's ast module to do the parsing for us?
+#
+# Result is a list of triples: [ start_line, end_line, function ].
+
+sub get_function_line_ranges_for_python($$)
+{
+    my ($fileHandle, $fileName) = @_;
+
+    my @ranges;
+
+    my @scopeStack = ({ line => 0, indent => -1, name => undef });
+    my $lastLine = 0;
+    until ($lastLine) {
+        $_ = <$fileHandle>;
+        unless ($_) {
+            # To pop out all popped scopes, run the loop once more after
+            # we encountered the end of the file.
+            $_ = "pass\n";
+            $.++;
+            $lastLine = 1;
+        }
+        chomp;
+        next unless /^(\s*)([^#].*)$/;
+
+        my $indent = length $1;
+        my $rest = $2;
+        my $scope = $scopeStack[-1];
+
+        if ($indent <= $scope->{indent}) {
+            # Find all the scopes that we have just exited.
+            my $i = 0;
+            for (; $i < @scopeStack; ++$i) {
+                last if $indent <= $scopeStack[$i]->{indent};
+            }
+            my @poppedScopes = splice @scopeStack, $i;
+
+            # For each scope that was just exited, add a range that goes from the start of that
+            # scope to the start of the next nested scope, or to the line just before this one for
+            # the innermost scope.
+            for ($i = 0; $i < @poppedScopes; ++$i) {
+                my $lineAfterEnd = $i + 1 == @poppedScopes ? $. : $poppedScopes[$i + 1]->{line};
+                push @ranges, [$poppedScopes[$i]->{line}, $lineAfterEnd - 1, $poppedScopes[$i]->{name}];
+            }
+            @scopeStack or warn "Popped off last scope at $fileName:$.\n";
+
+            # Set the now-current scope to start at the current line. Any lines within this scope
+            # before this point should already have been added to @ranges.
+            $scope = $scopeStack[-1];
+            $scope->{line} = $.;
+        }
+
+        next unless $rest =~ /(?:class|def)\s+(\w+)/;
+        my $name = $1;
+        my $fullName = $scope->{name} ? join('.', $scope->{name}, $name) : $name;
+        push @scopeStack, { line => $., indent => $indent, name => $fullName };
+
+        if ($scope->{indent} >= 0) {
+            push @ranges, [$scope->{line}, $. - 1, $scope->{name}];
+        }
+    }
+
+    return @ranges;
+}
+
+# Read a file and get all the line ranges of the things that look like CSS selectors.  A selector is
+# anything before an opening brace on a line. A selector starts at the line containing the opening
+# brace and ends at the closing brace.
+#
+# Result is a list of triples: [ start_line, end_line, selector ].
+
+sub get_selector_line_ranges_for_css($$)
+{
+    my ($fileHandle, $fileName) = @_;
+
+    my @ranges;
+
+    my $currentSelector = "";
+    my $start = 0;
+    my $inComment = 0;
+    my $inBrace = 0;
+
+    while (<$fileHandle>) {
+        foreach my $token (split m-(\{|\}|/\*|\*/)-, $_) {
+            if ($token eq "{") {
+                if (!$inComment) {
+                    warn "mismatched brace found in $fileName\n" if $inBrace;
+                    $inBrace = 1;
+                }
+            } elsif ($token eq "}") {
+                if (!$inComment) {
+                    warn "mismatched brace found in $fileName\n" if !$inBrace;
+                    $inBrace = 0;
+                    push(@ranges, [$start, $., $currentSelector]);
+                    $currentSelector = "";
+                    $start = 0;
+                }
+            } elsif ($token eq "/*") {
+                $inComment = 1;
+            } elsif ($token eq "*/") {
+                warn "mismatched comment found in $fileName\n" if !$inComment;
+                $inComment = 0;
+            } else {
+                if (!$inComment and !$inBrace and $token !~ /^[\s\t]*$/) {
+                    $token =~ s/^[\s\t]*|[\s\t]*$//g;
+                    $currentSelector = $token;
+                    $start = $.;
+                }
+            }
+        }
+    }
+
+    return @ranges;
+}
+
+sub processPaths(\@)
+{
+    my ($paths) = @_;
+    return ("." => 1) if (!@{$paths});
+
+    my %result = ();
+
+    for my $file (@{$paths}) {
+        die "can't handle absolute paths like \"$file\"\n" if File::Spec->file_name_is_absolute($file);
+        die "can't handle empty string path\n" if $file eq "";
+        die "can't handle path with single quote in the name like \"$file\"\n" if $file =~ /'/; # ' (keep Xcode syntax highlighting happy)
+
+        my $untouchedFile = $file;
+
+        $file = canonicalizePath($file);
+
+        die "can't handle paths with .. like \"$untouchedFile\"\n" if $file =~ m|/\.\./|;
+
+        $result{$file} = 1;
+    }
+
+    return ("." => 1) if ($result{"."});
+
+    # Remove any paths that also have a parent listed.
+    for my $path (keys %result) {
+        for (my $parent = dirname($path); $parent ne '.'; $parent = dirname($parent)) {
+            if ($result{$parent}) {
+                delete $result{$path};
+                last;
+            }
+        }
+    }
+
+    return %result;
+}
+
+sub diffFromToString($$$)
+{
+    my ($gitCommit, $gitIndex, $mergeBase) = @_;
+
+    return "" if isSVN();
+    return $gitCommit if $gitCommit =~ m/.+\.\..+/;
+    return "\"$gitCommit^\" \"$gitCommit\"" if $gitCommit;
+    return "--cached" if $gitIndex;
+    return $mergeBase if $mergeBase;
+    return "HEAD" if isGit();
+}
+
+sub diffCommand($$$$)
+{
+    my ($paths, $gitCommit, $gitIndex, $mergeBase) = @_;
+
+    my $command;
+    if (isSVN()) {
+        my @escapedPaths = map(escapeSubversionPath($_), @$paths);
+        my $escapedPathsString = "'" . join("' '", @escapedPaths) . "'";
+        $command = SVN . " diff --diff-cmd diff -x -N $escapedPathsString";
+    } elsif (isGit()) {
+        my $pathsString = "'" . join("' '", @$paths) . "'"; 
+        $command = GIT . " diff --no-ext-diff -U0 " . diffFromToString($gitCommit, $gitIndex, $mergeBase);
+        $command .= " -- $pathsString" unless $gitCommit or $mergeBase;
+    }
+
+    return $command;
+}
+
+sub statusCommand($$$$)
+{
+    my ($paths, $gitCommit, $gitIndex, $mergeBase) = @_;
+
+    my $command;
+    if (isSVN()) {
+        my @escapedFiles = map(escapeSubversionPath($_), keys %$paths);
+        my $escapedFilesString = "'" . join("' '", @escapedFiles) . "'";
+        $command = SVN . " stat $escapedFilesString";
+    } elsif (isGit()) {
+        my $filesString = '"' . join('" "', keys %$paths) . '"';
+        $command = GIT . " diff -r --name-status -M -C " . diffFromToString($gitCommit, $gitIndex, $mergeBase);
+        $command .= " -- $filesString" unless $gitCommit;
+    }
+
+    return "$command 2>&1";
+}
+
+sub createPatchCommand($$$$)
+{
+    my ($changedFilesString, $gitCommit, $gitIndex, $mergeBase) = @_;
+
+    my $command;
+    if (isSVN()) {
+        $command = "'$FindBin::Bin/svn-create-patch' $changedFilesString";
+    } elsif (isGit()) {
+        $command = GIT . " diff -M -C " . diffFromToString($gitCommit, $gitIndex, $mergeBase);
+        $command .= " -- $changedFilesString" unless $gitCommit;
+    }
+
+    return $command;
+}
+
+sub diffHeaderFormat()
+{
+    return qr/^Index: (\S+)[\r\n]*$/ if isSVN();
+    return qr/^diff --git a\/.+ b\/(.+)$/ if isGit();
+}
+
+sub findOriginalFileFromSvn($)
+{
+    my ($file) = @_;
+    my $baseUrl;
+    open INFO, SVN . " info . |" or die;
+    while (<INFO>) {
+        if (/^URL: (.+?)[\r\n]*$/) {
+            $baseUrl = $1;
+        }
+    }
+    close INFO;
+    my $sourceFile;
+    my $escapedFile = escapeSubversionPath($file);
+    open INFO, SVN . " info '$escapedFile' |" or die;
+    while (<INFO>) {
+        if (/^Copied From URL: (.+?)[\r\n]*$/) {
+            $sourceFile = File::Spec->abs2rel($1, $baseUrl);
+        }
+    }
+    close INFO;
+    return $sourceFile;
+}
+
+sub determinePropertyChanges($$$)
+{
+    my ($file, $isAdd, $original) = @_;
+
+    my $escapedFile = escapeSubversionPath($file);
+    my %changes;
+    if ($isAdd) {
+        my %addedProperties;
+        my %removedProperties;
+        open PROPLIST, SVN . " proplist '$escapedFile' |" or die;
+        while (<PROPLIST>) {
+            $addedProperties{$1} = 1 if /^  (.+?)[\r\n]*$/ && $1 ne 'svn:mergeinfo';
+        }
+        close PROPLIST;
+        if ($original) {
+            my $escapedOriginal = escapeSubversionPath($original);
+            open PROPLIST, SVN . " proplist '$escapedOriginal' |" or die;
+            while (<PROPLIST>) {
+                next unless /^  (.+?)[\r\n]*$/;
+                my $property = $1;
+                if (exists $addedProperties{$property}) {
+                    delete $addedProperties{$1};
+                } else {
+                    $removedProperties{$1} = 1;
+                }
+            }
+        }
+        $changes{"A"} = [sort keys %addedProperties] if %addedProperties;
+        $changes{"D"} = [sort keys %removedProperties] if %removedProperties;
+    } else {
+        open DIFF, SVN . " diff '$escapedFile' |" or die;
+        while (<DIFF>) {
+            if (/^Property changes on:/) {
+                while (<DIFF>) {
+                    my $operation;
+                    my $property;
+                    if (/^Added: (\S*)/) {
+                        $operation = "A";
+                        $property = $1;
+                    } elsif (/^Modified: (\S*)/) {
+                        $operation = "M";
+                        $property = $1;
+                    } elsif (/^Deleted: (\S*)/) {
+                        $operation = "D";
+                        $property = $1;
+                    } elsif (/^Name: (\S*)/) {
+                        # Older versions of svn just say "Name" instead of the type
+                        # of property change.
+                        $operation = "C";
+                        $property = $1;
+                    }
+                    if ($operation) {
+                        $changes{$operation} = [] unless exists $changes{$operation};
+                        push @{$changes{$operation}}, $property;
+                    }
+                }
+            }
+        }
+        close DIFF;
+    }
+    return \%changes;
+}
+
+sub pluralizeAndList($$@)
+{
+    my ($singular, $plural, @items) = @_;
+
+    return if @items == 0;
+    return "$singular $items[0]" if @items == 1;
+    return "$plural " . join(", ", @items[0 .. $#items - 1]) . " and " . $items[-1];
+}
+
+sub generateFileList(\%$$$)
+{
+    my ($paths, $gitCommit, $gitIndex, $mergeBase) = @_;
+
+    my @changedFiles;
+    my @conflictFiles;
+    my %functionLists;
+    my @addedRegressionTests;
+    print STDERR "  Running status to find changed, added, or removed files.\n";
+    open STAT, "-|", statusCommand($paths, $gitCommit, $gitIndex, $mergeBase) or die "The status failed: $!.\n";
+    while (<STAT>) {
+        my $status;
+        my $propertyStatus;
+        my $propertyChanges;
+        my $original;
+        my $file;
+
+        if (isSVN()) {
+            my $matches;
+            if (isSVNVersion16OrNewer()) {
+                $matches = /^([ ACDMR])([ CM]).{5} (.+?)[\r\n]*$/;
+                $status = $1;
+                $propertyStatus = $2;
+                $file = $3;
+            } else {
+                $matches = /^([ ACDMR])([ CM]).{4} (.+?)[\r\n]*$/;
+                $status = $1;
+                $propertyStatus = $2;
+                $file = $3;
+            }
+            if ($matches) {
+                $file = normalizePath($file);
+                $original = findOriginalFileFromSvn($file) if substr($_, 3, 1) eq "+";
+                my $isAdd = isAddedStatus($status);
+                $propertyChanges = determinePropertyChanges($file, $isAdd, $original) if isModifiedStatus($propertyStatus) || $isAdd;
+            } else {
+                print;  # error output from svn stat
+            }
+        } elsif (isGit()) {
+            if (/^([ADM])\t(.+)$/) {
+                $status = $1;
+                $propertyStatus = " ";  # git doesn't have properties
+                $file = normalizePath($2);
+            } elsif (/^([CR])[0-9]{1,3}\t([^\t]+)\t([^\t\n]+)$/) { # for example: R90%    newfile    oldfile
+                $status = $1;
+                $propertyStatus = " ";
+                $original = normalizePath($2);
+                $file = normalizePath($3);
+            } else {
+                print;  # error output from git diff
+            }
+        }
+
+        next if !$status || isUnmodifiedStatus($status) && isUnmodifiedStatus($propertyStatus);
+
+        $file = makeFilePathRelative($file);
+
+        if (isModifiedStatus($status) || isAddedStatus($status) || isModifiedStatus($propertyStatus)) {
+            my @components = File::Spec->splitdir($file);
+            if ($components[0] eq "LayoutTests") {
+                push @addedRegressionTests, $file
+                    if isAddedStatus($status)
+                       && $file =~ /\.([a-zA-Z]+)$/
+                       && SupportedTestExtensions->{lc($1)}
+                       && $file !~ /-expected(-mismatch)?\.html$/
+                       && !scalar(grep(/^resources$/i, @components))
+                       && !scalar(grep(/^script-tests$/i, @components));
+            }
+            push @changedFiles, $file if $components[$#components] ne changeLogFileName();
+        } elsif (isConflictStatus($status, $gitCommit, $gitIndex) || isConflictStatus($propertyStatus, $gitCommit, $gitIndex)) {
+            push @conflictFiles, $file;
+        }
+        if (basename($file) ne changeLogFileName()) {
+            my $description = statusDescription($status, $propertyStatus, $original, $propertyChanges);
+            $functionLists{$file} = $description if defined $description;
+        }
+    }
+    close STAT;
+    return (\@changedFiles, \@conflictFiles, \%functionLists, \@addedRegressionTests);
+}
+
+sub isUnmodifiedStatus($)
+{
+    my ($status) = @_;
+
+    my %statusCodes = (
+        " " => 1,
+    );
+
+    return $statusCodes{$status};
+}
+
+sub isModifiedStatus($)
+{
+    my ($status) = @_;
+
+    my %statusCodes = (
+        "M" => 1,
+    );
+
+    return $statusCodes{$status};
+}
+
+sub isAddedStatus($)
+{
+    my ($status) = @_;
+
+    my %statusCodes = (
+        "A" => 1,
+        "C" => isGit(),
+        "R" => 1,
+    );
+
+    return $statusCodes{$status};
+}
+
+sub isConflictStatus($$$)
+{
+    my ($status, $gitCommit, $gitIndex) = @_;
+
+    my %svn = (
+        "C" => 1,
+    );
+
+    my %git = (
+        "U" => 1,
+    );
+
+    return 0 if ($gitCommit || $gitIndex); # an existing commit or staged change cannot have conflicts
+    return $svn{$status} if isSVN();
+    return $git{$status} if isGit();
+}
+
+sub statusDescription($$$$)
+{
+    my ($status, $propertyStatus, $original, $propertyChanges) = @_;
+
+    my $propertyDescription = defined $propertyChanges ? propertyChangeDescription($propertyChanges) : "";
+
+    my %svn = (
+        "A" => defined $original ? " Copied from \%s." : " Added.",
+        "D" => " Removed.",
+        "M" => "",
+        "R" => defined $original ? " Replaced with \%s." : " Replaced.",
+        " " => "",
+    );
+
+    my %git = %svn;
+    $git{"A"} = " Added.";
+    $git{"C"} = " Copied from \%s.";
+    $git{"R"} = " Renamed from \%s.";
+
+    my $description;
+    $description = sprintf($svn{$status}, $original) if isSVN() && exists $svn{$status};
+    $description = sprintf($git{$status}, $original) if isGit() && exists $git{$status};
+    return unless defined $description;
+
+    $description .= $propertyDescription unless isAddedStatus($status);
+    return $description;
+}
+
+sub propertyChangeDescription($)
+{
+    my ($propertyChanges) = @_;
+
+    my %operations = (
+        "A" => "Added",
+        "M" => "Modified",
+        "D" => "Removed",
+        "C" => "Changed",
+    );
+
+    my $description = "";
+    while (my ($operation, $properties) = each %$propertyChanges) {
+        my $word = $operations{$operation};
+        my $list = pluralizeAndList("property", "properties", @$properties);
+        $description .= " $word $list.";
+    }
+    return $description;
+}
+
+sub extractLineRange($)
+{
+    my ($string) = @_;
+
+    my ($start, $end) = (-1, -1);
+
+    if (isSVN() && $string =~ /^\d+(,\d+)?[acd](\d+)(,(\d+))?/) {
+        $start = $2;
+        $end = $4 || $2;
+    } elsif (isGit() && $string =~ /^@@ -\d+(,\d+)? \+(\d+)(,(\d+))? @@/) {
+        $start = $2;
+        $end = defined($4) ? $4 + $2 - 1 : $2;
+    }
+
+    return ($start, $end);
+}
+
+sub testListForChangeLog(@)
+{
+    my (@tests) = @_;
+
+    return "" unless @tests;
+
+    my $leadString = "        Test" . (@tests == 1 ? "" : "s") . ": ";
+    my $list = $leadString;
+    foreach my $i (0..$#tests) {
+        $list .= " " x length($leadString) if $i;
+        my $test = $tests[$i];
+        $test =~ s/^LayoutTests\///;
+        $list .= "$test\n";
+    }
+    $list .= "\n";
+
+    return $list;
+}
+
+sub reviewerAndDescriptionForGitCommit($$)
+{
+    my ($commit, $gitReviewer) = @_;
+
+    my $description = '';
+    my $reviewer;
+
+    my @args = qw(rev-list --pretty);
+    push @args, '-1' if $commit !~ m/.+\.\..+/;
+    my $gitLog;
+    {
+        local $/ = undef;
+        open(GITLOG, "-|", GIT, @args, $commit) || die;
+        $gitLog = <GITLOG>;
+        close(GITLOG);
+    }
+
+    my @commitLogs = split(/^[Cc]ommit [a-f0-9]{40}/m, $gitLog);
+    shift @commitLogs; # Remove initial blank commit log
+    my $commitLogCount = 0;
+    foreach my $commitLog (@commitLogs) {
+        $description .= "\n" if $commitLogCount;
+        $commitLogCount++;
+        my $inHeader = 1;
+        my $commitLogIndent; 
+        my @lines = split(/\n/, $commitLog);
+        shift @lines; # Remove initial blank line
+        foreach my $line (@lines) {
+            if ($inHeader) {
+                if (!$line) {
+                    $inHeader = 0;
+                }
+                next;
+            } elsif ($line =~ /[Ss]igned-[Oo]ff-[Bb]y: (.+)/) {
+                if (!$reviewer) {
+                    $reviewer = $1;
+                } else {
+                    $reviewer .= ", " . $1;
+                }
+            } elsif ($line =~ /^\s*$/) {
+                $description = $description . "\n";
+            } else {
+                if (!defined($commitLogIndent)) {
+                    # Let the first line with non-white space determine
+                    # the global indent.
+                    $line =~ /^(\s*)\S/;
+                    $commitLogIndent = length($1);
+                }
+                # Strip at most the indent to preserve relative indents.
+                $line =~ s/^\s{0,$commitLogIndent}//;
+                $description = $description . (" " x 8) . $line . "\n";
+            }
+        }
+    }
+    if (!$reviewer) {
+      $reviewer = $gitReviewer;
+    }
+
+    return ($reviewer, $description);
+}
+
+sub normalizeLineEndings($$)
+{
+    my ($string, $endl) = @_;
+    $string =~ s/\r?\n/$endl/g;
+    return $string;
+}
+
+sub decodeEntities($)
+{
+    my ($text) = @_;
+    $text =~ s/\&lt;/</g;
+    $text =~ s/\&gt;/>/g;
+    $text =~ s/\&quot;/\"/g;
+    $text =~ s/\&apos;/\'/g;
+    $text =~ s/\&amp;/\&/g;
+    return $text;
+}
diff --git a/Tools/Scripts/print-msvc-project-dependencies b/Tools/Scripts/print-msvc-project-dependencies
new file mode 100755
index 0000000..dbc8402
--- /dev/null
+++ b/Tools/Scripts/print-msvc-project-dependencies
@@ -0,0 +1,143 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2008 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. ``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
+# 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;
+
+sub printDependencyTree($);
+
+my $basename = basename($0);
+@ARGV or die "Usage: $basename sln1 [sln2 sln3...]";
+
+foreach my $sln (@ARGV) {
+    printDependencyTree($sln);
+}
+
+exit;
+
+sub printDependencyTree($)
+{
+    my ($sln) = @_;
+
+    unless (-f $sln) {
+        warn "Warning: Can't find $sln; skipping\n";
+        return;
+    }
+
+    unless (open SLN, "<", $sln) {
+        warn "Warning: Can't open $sln; skipping\n";
+        return;
+    }
+
+    my %projectsByUUID = ();
+    my $currentProject;
+
+    my $state = "initial";
+    foreach my $line (<SLN>) {
+        if ($state eq "initial") {
+            if ($line =~ /^Project\([^\)]+\) = "([^"]+)", "[^"]+", "([^"]+)"\r?$/) {
+                my $name = $1;
+                my $uuid = $2;
+                if (exists $projectsByUUID{$uuid}) {
+                    warn "Warning: Project $name appears more than once in $sln; using first definition\n";
+                    next;
+                }
+                $currentProject = {
+                    name => $name,
+                    uuid => $uuid,
+                    dependencies => {},
+                };
+                $projectsByUUID{$uuid} = $currentProject;
+
+                $state = "inProject";
+            }
+
+            next;
+        }
+
+        if ($state eq "inProject") {
+            defined($currentProject) or die;
+
+            if ($line =~ /^\s*ProjectSection\(ProjectDependencies\) = postProject\r?$/) {
+                $state = "inDependencies";
+            } elsif ($line =~ /^EndProject\r?$/) {
+                $currentProject = undef;
+                $state = "initial";
+            }
+
+            next;
+        }
+
+        if ($state eq "inDependencies") {
+            defined($currentProject) or die;
+
+            if ($line =~ /^\s*({[^}]+}) = ({[^}]+})\r?$/) {
+                my $uuid1 = $1;
+                my $uuid2 = $2;
+                if (exists $currentProject->{dependencies}->{$uuid1}) {
+                    warn "Warning: UUID $uuid1 listed more than once as dependency of project ", $currentProject->{name}, "\n";
+                    next;
+                }
+
+                $uuid1 eq $uuid2 or warn "Warning: UUIDs in depedency section of project ", $currentProject->{name}, " don't match: $uuid1 $uuid2; using first UUID\n";
+
+                $currentProject->{dependencies}->{$uuid1} = 1;
+            } elsif ($line =~ /^\s*EndProjectSection\r?$/) {
+                $state = "inProject";
+            }
+
+            next;
+        }
+    }
+
+    close SLN or warn "Warning: Can't close $sln\n";
+
+    my %projectsNotDependedUpon = %projectsByUUID;
+    CANDIDATE: foreach my $candidateUUID (keys %projectsByUUID) {
+        foreach my $projectUUID (keys %projectsByUUID) {
+            next if $candidateUUID eq $projectUUID;
+            foreach my $dependencyUUID (keys %{$projectsByUUID{$projectUUID}->{dependencies}}) {
+                if ($candidateUUID eq $dependencyUUID) {
+                    delete $projectsNotDependedUpon{$candidateUUID};
+                    next CANDIDATE;
+                }
+            }
+        }
+    }
+
+    foreach my $project (values %projectsNotDependedUpon) {
+        printProjectAndDependencies($project, 0, \%projectsByUUID);
+    }
+}
+
+sub printProjectAndDependencies
+{
+    my ($project, $indentLevel, $projectsByUUID) = @_;
+
+    print " " x $indentLevel, $project->{name}, "\n";
+    foreach my $dependencyUUID (keys %{$project->{dependencies}}) {
+        printProjectAndDependencies($projectsByUUID->{$dependencyUUID}, $indentLevel + 1, $projectsByUUID);
+    }
+}
diff --git a/Tools/Scripts/print-vse-failure-logs b/Tools/Scripts/print-vse-failure-logs
new file mode 100755
index 0000000..7580465
--- /dev/null
+++ b/Tools/Scripts/print-vse-failure-logs
@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# This is a very simple script designed to crawl the build directory
+# for visual studio express build logs and print them to stdout.
+
+from __future__ import with_statement
+
+import codecs
+import os
+import re
+
+from webkitpy.common.checkout import scm
+from webkitpy.common.system.executive import Executive
+from webkitpy.thirdparty import BeautifulSoup
+
+
+class PrintVisualStudioExpressLogs(object):
+    def __init__(self):
+        self._executive = Executive()
+
+    def _find_buildlogs(self, build_directory):
+        build_log_paths = []
+        for dirpath, dirnames, filenames in os.walk(build_directory):
+            for file_name in filenames:
+                if file_name == "BuildLog.htm":
+                    file_path = os.path.join(dirpath, file_name)
+                    build_log_paths.append(file_path)
+        return build_log_paths
+
+    def _build_order(self):
+        """Returns a list of project names in the order in which they are built."""
+        script_path = os.path.join(self._scripts_directory(), "print-msvc-project-dependencies")
+        sln_path = os.path.join(scm.find_checkout_root(), "WebKit", "win", "WebKit.vcproj", "WebKit.sln")
+        lines = self._executive.run_command([script_path, sln_path]).splitlines()
+        order = [line.strip() for line in lines if line.find("Folder") == -1]
+        order.reverse()
+        return order
+
+    def _sort_buildlogs(self, log_paths):
+        build_order = self._build_order()
+        def sort_key(log_path):
+            project_name = os.path.basename(os.path.dirname(os.path.dirname(log_path)))
+            try:
+                index = build_order.index(project_name)
+            except ValueError:
+                # If the project isn't in the list, sort it after all items that
+                # are in the list.
+                index = len(build_order)
+            # Sort first by build order, then by project name
+            return (index, project_name)
+        return sorted(log_paths, key=sort_key)
+
+    def _obj_directory(self):
+        build_directory_script_path = os.path.join(self._scripts_directory(), "webkit-build-directory")
+        # FIXME: ports/webkit.py should provide the build directory in a nice API.
+        # NOTE: The windows VSE build does not seem to use different directories
+        # for Debug and Release.
+        build_directory = self._executive.run_command([build_directory_script_path, "--top-level"]).rstrip()
+        return os.path.join(build_directory, "obj")
+
+    def _scripts_directory(self):
+        return os.path.dirname(__file__)
+
+    def _relevant_text(self, log):
+        soup = BeautifulSoup.BeautifulSoup(log)
+        # The Output Window table is where the useful output starts in the build log.
+        output_window_table = soup.find(text=re.compile("Output Window")).findParent("table")
+        result = []
+        for table in [output_window_table] + output_window_table.findNextSiblings("table"):
+            result.extend([text.replace("&nbsp;", "") for text in table.findAll(text=True)])
+            result.append("\n")
+        return "".join(result)
+
+    def main(self):
+        build_log_paths = self._sort_buildlogs(self._find_buildlogs(self._obj_directory()))
+
+        print "Found %s Visual Studio Express Build Logs:\n%s" % (len(build_log_paths), "\n".join(build_log_paths))
+
+        for build_log_path in build_log_paths:
+            print "%s:\n" % build_log_path
+            with codecs.open(build_log_path, "r", "utf-16") as build_log:
+                print self._relevant_text(build_log)
+
+
+if __name__ == '__main__':
+    PrintVisualStudioExpressLogs().main()
diff --git a/Tools/Scripts/read-checksum-from-png b/Tools/Scripts/read-checksum-from-png
new file mode 100755
index 0000000..fb03f28
--- /dev/null
+++ b/Tools/Scripts/read-checksum-from-png
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+# Copyright (c) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import with_statement
+import sys
+
+from webkitpy.common import read_checksum_from_png
+
+
+if '__main__' == __name__:
+    for filename in sys.argv[1:]:
+        with open(filename, 'r') as filehandle:
+            print "%s: %s" % (read_checksum_from_png.read_checksum(filehandle), filename)
diff --git a/Tools/Scripts/report-include-statistics b/Tools/Scripts/report-include-statistics
new file mode 100755
index 0000000..17152ab
--- /dev/null
+++ b/Tools/Scripts/report-include-statistics
@@ -0,0 +1,114 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005, 2006 Apple Computer, Inc.  All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer. 
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution. 
+# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+#     its contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission. 
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# "report-include-statistics" script for WebKit Open Source Project
+
+use strict;
+use File::Find;
+
+find(\&wanted, @ARGV ? @ARGV : ".");
+
+my %paths;
+my %sources;
+my %includes;
+
+sub wanted
+{
+    my $file = $_;
+
+    if ($file eq "icu") {
+        $File::Find::prune = 1;
+        return;
+    }
+
+    if ($file !~ /^\./ && $file =~ /\.(h|cpp|c|mm|m)$/) {
+        $paths{$file} = $File::Find::name;
+        $sources{$file} = $File::Find::name if $file !~ /\.h/;
+        open FILE, $file or die;
+        while (<FILE>) {
+            if (m-^\s*#\s*(include|import)\s+["<]((\S+/)*)(\S+)[">]-) {
+                my $include = ($2 eq "sys/" ? $2 : "") . $4;
+                $includes{$file}{$include}++;
+            }
+        }
+        close FILE;
+    }
+}
+
+my %totalIncludes;
+
+sub fillOut
+{
+    my ($file) = @_;
+
+    return if defined $totalIncludes{$file};
+
+    for my $include (keys %{ $includes{$file} }) {
+        $totalIncludes{$file}{$include} = 1;
+        fillOut($include);
+        for my $i (keys %{ $totalIncludes{$include} }) {
+            $totalIncludes{$file}{$i} = 1;
+        }
+    }
+}
+
+my %inclusionCounts;
+for my $file (keys %includes) {
+    $inclusionCounts{$file} = 0;
+    fillOut($file);
+}
+
+for my $file (keys %sources) {
+    for my $include (keys %{ $totalIncludes{$file} }) {
+        $inclusionCounts{$include}++;
+    }
+}
+
+for my $file (sort mostincludedcmp keys %includes) {
+    next if !$paths{$file};
+    my $count = $inclusionCounts{$file};
+    my $numIncludes = keys %{ $includes{$file} };
+    my $numTotalIncludes = keys %{ $totalIncludes{$file} };
+    print "$file is included $count times, includes $numIncludes files directly, $numTotalIncludes files total.\n"
+}
+
+# Sort most-included files first.
+sub mostincludedcmp($$)
+{
+    my ($filea, $fileb) = @_;
+
+    my $counta = $inclusionCounts{$filea} || 0;
+    my $countb = $inclusionCounts{$fileb} || 0;
+    return $countb <=> $counta if $counta != $countb;
+
+    my $ta = keys %{ $totalIncludes{$filea} };
+    my $tb = keys %{ $totalIncludes{$fileb} };
+    return $ta <=> $tb if $ta != $tb;
+
+    return $filea cmp $fileb;
+}
diff --git a/Tools/Scripts/resolve-ChangeLogs b/Tools/Scripts/resolve-ChangeLogs
new file mode 100755
index 0000000..2206828
--- /dev/null
+++ b/Tools/Scripts/resolve-ChangeLogs
@@ -0,0 +1,496 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2007, 2008, 2009 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.
+
+# Merge and resolve ChangeLog conflicts for svn and git repositories
+
+use strict;
+
+use FindBin;
+use lib $FindBin::Bin;
+
+use File::Basename;
+use File::Copy;
+use File::Path;
+use File::Spec;
+use Getopt::Long;
+use POSIX;
+use VCSUtils;
+
+sub canonicalRelativePath($);
+sub conflictFiles($);
+sub findChangeLog($);
+sub findUnmergedChangeLogs();
+sub fixMergedChangeLogs($;@);
+sub fixOneMergedChangeLog($);
+sub hasGitUnmergedFiles();
+sub isInGitFilterBranch();
+sub parseFixMerged($$;$);
+sub removeChangeLogArguments($);
+sub resolveChangeLog($);
+sub resolveConflict($);
+sub showStatus($;$);
+sub usageAndExit();
+
+my $isGit = isGit();
+my $isSVN = isSVN();
+
+my $SVN = "svn";
+my $GIT = "git";
+
+my $fixMerged;
+my $gitRebaseContinue = 0;
+my $mergeDriver = 0;
+my $printWarnings = 1;
+my $showHelp;
+
+sub usageAndExit()
+{
+    print STDERR <<__END__;
+Usage: @{[ basename($0) ]} [options] [path/to/ChangeLog] [path/to/another/ChangeLog ...]
+  -c|--[no-]continue               run "git rebase --continue" after fixing ChangeLog
+                                   entries (default: --no-continue)
+  -f|--fix-merged [revision-range] fix git-merged ChangeLog entries; if a revision-range
+                                   is specified, run git filter-branch on the range
+  -m|--merge-driver %O %A %B       act as a git merge-driver on files %O %A %B
+  -h|--help                        show this help message
+  -w|--[no-]warnings               show or suppress warnings (default: show warnings)
+__END__
+    exit 1;
+}
+
+my $getOptionsResult = GetOptions(
+    'c|continue!'     => \$gitRebaseContinue,
+    'f|fix-merged:s'  => \&parseFixMerged,
+    'm|merge-driver!' => \$mergeDriver,
+    'h|help'          => \$showHelp,
+    'w|warnings!'     => \$printWarnings,
+);
+
+if (!$getOptionsResult || $showHelp) {
+    usageAndExit();
+}
+
+my $relativePath = isInGitFilterBranch() ? '.' : chdirReturningRelativePath(determineVCSRoot());
+
+my @changeLogFiles = removeChangeLogArguments($relativePath);
+
+if (!defined $fixMerged && !$mergeDriver && scalar(@changeLogFiles) == 0) {
+    @changeLogFiles = findUnmergedChangeLogs();
+}
+
+if (!$mergeDriver && scalar(@ARGV) > 0) {
+    print STDERR "ERROR: Files listed on command-line that are not ChangeLogs.\n";
+    undef $getOptionsResult;
+} elsif (!defined $fixMerged && !$mergeDriver && scalar(@changeLogFiles) == 0) {
+    print STDERR "ERROR: No ChangeLog files listed on command-line or found unmerged.\n";
+    undef $getOptionsResult;
+} elsif ($gitRebaseContinue && !$isGit) {
+    print STDERR "ERROR: --continue may only be used with a git repository\n";
+    undef $getOptionsResult;
+} elsif (defined $fixMerged && !$isGit) {
+    print STDERR "ERROR: --fix-merged may only be used with a git repository\n";
+    undef $getOptionsResult;
+} elsif ($mergeDriver && !$isGit) {
+    print STDERR "ERROR: --merge-driver may only be used with a git repository\n";
+    undef $getOptionsResult;
+} elsif ($mergeDriver && scalar(@ARGV) < 3) {
+    print STDERR "ERROR: --merge-driver expects %O %A %B as arguments\n";
+    undef $getOptionsResult;
+}
+
+if (!$getOptionsResult) {
+    usageAndExit();
+}
+
+if (defined $fixMerged && length($fixMerged) > 0) {
+    my $commitRange = $fixMerged;
+    $commitRange = $commitRange . "..HEAD" if index($commitRange, "..") < 0;
+    fixMergedChangeLogs($commitRange, @changeLogFiles);
+} elsif ($mergeDriver) {
+    my ($base, $theirs, $ours) = @ARGV;
+    if (mergeChangeLogs($ours, $base, $theirs)) {
+        unlink($ours);
+        copy($theirs, $ours) or die $!;
+    } else {
+        exec qw(git merge-file -L THEIRS -L BASE -L OURS), $theirs, $base, $ours;
+    }
+} elsif (@changeLogFiles) {
+    for my $file (@changeLogFiles) {
+        if (defined $fixMerged) {
+            fixOneMergedChangeLog($file);
+        } else {
+            resolveChangeLog($file);
+        }
+    }
+} else {
+    print STDERR "ERROR: Unknown combination of switches and arguments.\n";
+    usageAndExit();
+}
+
+if ($gitRebaseContinue) {
+    if (hasGitUnmergedFiles()) {
+        print "Unmerged files; skipping '$GIT rebase --continue'.\n";
+    } else {
+        print "Running '$GIT rebase --continue'...\n";
+        print `$GIT rebase --continue`;
+    }
+}
+
+exit 0;
+
+sub canonicalRelativePath($)
+{
+    my ($originalPath) = @_;
+    my $absolutePath = Cwd::abs_path($originalPath);
+    return File::Spec->abs2rel($absolutePath, Cwd::getcwd());
+}
+
+sub conflictFiles($)
+{
+    my ($file) = @_;
+    my $fileMine;
+    my $fileOlder;
+    my $fileNewer;
+
+    if (-e $file && -e "$file.orig" && -e "$file.rej") {
+        return ("$file.rej", "$file.orig", $file);
+    }
+
+    if ($isSVN) {
+        my $escapedFile = escapeSubversionPath($file);
+        open STAT, "-|", $SVN, "status", $escapedFile or die $!;
+        my $status = <STAT>;
+        close STAT;
+        if (!$status || $status !~ m/^C\s+/) {
+            print STDERR "WARNING: ${file} is not in a conflicted state.\n" if $printWarnings;
+            return ();
+        }
+
+        $fileMine = "${file}.mine" if -e "${file}.mine";
+
+        my $currentRevision;
+        open INFO, "-|", $SVN, "info", $escapedFile or die $!;
+        while (my $line = <INFO>) {
+            if ($line =~ m/^Revision: ([0-9]+)/) {
+                $currentRevision = $1;
+                { local $/ = undef; <INFO>; }  # Consume rest of input.
+            }
+        }
+        close INFO;
+        $fileNewer = "${file}.r${currentRevision}" if -e "${file}.r${currentRevision}";
+
+        my @matchingFiles = grep { $_ ne $fileNewer } glob("${file}.r[0-9][0-9]*");
+        if (scalar(@matchingFiles) > 1) {
+            print STDERR "WARNING: Too many conflict files exist for ${file}!\n" if $printWarnings;
+        } else {
+            $fileOlder = shift @matchingFiles;
+        }
+    } elsif ($isGit) {
+        my $gitPrefix = `$GIT rev-parse --show-prefix`;
+        chomp $gitPrefix;
+        open GIT, "-|", $GIT, "ls-files", "--unmerged", $file or die $!;
+        while (my $line = <GIT>) {
+            my ($mode, $hash, $stage, $fileName) = split(' ', $line);
+            my $outputFile;
+            if ($stage == 1) {
+                $fileOlder = "${file}.BASE.$$";
+                $outputFile = $fileOlder;
+            } elsif ($stage == 2) {
+                $fileNewer = "${file}.LOCAL.$$";
+                $outputFile = $fileNewer;
+            } elsif ($stage == 3) {
+                $fileMine = "${file}.REMOTE.$$";
+                $outputFile = $fileMine;
+            } else {
+                die "Unknown file stage: $stage";
+            }
+            system("$GIT cat-file blob :${stage}:${gitPrefix}${file} > $outputFile");
+            die $! if WEXITSTATUS($?);
+        }
+        close GIT or die $!;
+    } else {
+        die "Unknown version control system";
+    }
+
+    if (!$fileMine && !$fileOlder && !$fileNewer) {
+        print STDERR "WARNING: ${file} does not need merging.\n" if $printWarnings;
+    } elsif (!$fileMine || !$fileOlder || !$fileNewer) {
+        print STDERR "WARNING: ${file} is missing some conflict files.\n" if $printWarnings;
+    }
+
+    return ($fileMine, $fileOlder, $fileNewer);
+}
+
+sub findChangeLog($)
+{
+    my $changeLogFileName = changeLogFileName();
+    return $_[0] if basename($_[0]) eq $changeLogFileName;
+
+    my $file = File::Spec->catfile($_[0], $changeLogFileName);
+    return $file if -d $_[0] and -e $file;
+
+    return undef;
+}
+
+sub findUnmergedChangeLogs()
+{
+    my $statCommand = "";
+
+    if ($isSVN) {
+        $statCommand = "$SVN stat | grep '^C'";
+    } elsif ($isGit) {
+        $statCommand = "$GIT diff -r --name-status --diff-filter=U -C -C -M";
+    } else {
+        return ();
+    }
+
+    my @results = ();
+    open STAT, "-|", $statCommand or die "The status failed: $!.\n";
+    while (<STAT>) {
+        if ($isSVN) {
+            my $matches;
+            my $file;
+            if (isSVNVersion16OrNewer()) {
+                $matches = /^([C]).{6} (.+?)[\r\n]*$/;
+                $file = $2;
+            } else {
+                $matches = /^([C]).{5} (.+?)[\r\n]*$/;
+                $file = $2;
+            }
+            if ($matches) {
+                $file = findChangeLog(normalizePath($file));
+                push @results, $file if $file;
+            } else {
+                print;  # error output from svn stat
+            }
+        } elsif ($isGit) {
+            if (/^([U])\t(.+)$/) {
+                my $file = findChangeLog(normalizePath($2));
+                push @results, $file if $file;
+            } else {
+                print;  # error output from git diff
+            }
+        }
+    }
+    close STAT;
+
+    return @results;
+}
+
+sub fixMergedChangeLogs($;@)
+{
+    my $revisionRange = shift;
+    my @changedFiles = @_;
+
+    if (scalar(@changedFiles) < 1) {
+        # Read in list of files changed in $revisionRange
+        open GIT, "-|", $GIT, "diff", "--name-only", $revisionRange or die $!;
+        push @changedFiles, <GIT>;
+        close GIT or die $!;
+        die "No changed files in $revisionRange" if scalar(@changedFiles) < 1;
+        chomp @changedFiles;
+    }
+
+    my @changeLogs = grep { defined $_ } map { findChangeLog($_) } @changedFiles;
+    die "No changed ChangeLog files in $revisionRange" if scalar(@changeLogs) < 1;
+
+    system("$GIT filter-branch --tree-filter 'PREVIOUS_COMMIT=\`$GIT rev-parse \$GIT_COMMIT^\` && MAPPED_PREVIOUS_COMMIT=\`map \$PREVIOUS_COMMIT\` \"$0\" -f \"" . join('" "', @changeLogs) . "\"' $revisionRange");
+
+    # On success, remove the backup refs directory
+    if (WEXITSTATUS($?) == 0) {
+        rmtree(qw(.git/refs/original));
+    }
+}
+
+sub fixOneMergedChangeLog($)
+{
+    my $file = shift;
+    my $patch;
+
+    # Read in patch for incorrectly merged ChangeLog entry
+    {
+        local $/ = undef;
+        open GIT, "-|", $GIT, "diff", ($ENV{GIT_COMMIT} || "HEAD") . "^", $file or die $!;
+        $patch = <GIT>;
+        close GIT or die $!;
+    }
+
+    # Always checkout the previous commit's copy of the ChangeLog
+    system($GIT, "checkout", $ENV{MAPPED_PREVIOUS_COMMIT} || "HEAD^", $file);
+    die $! if WEXITSTATUS($?);
+
+    # The patch must have 0 or more lines of context, then 1 or more lines
+    # of additions, and then 1 or more lines of context.  If not, we skip it.
+    if ($patch =~ /\n@@ -(\d+),(\d+) \+(\d+),(\d+) @@\n( .*\n)*((\+.*\n)+)( .*\n)+$/m) {
+        # Copy the header from the original patch.
+        my $newPatch = substr($patch, 0, index($patch, "@@ -${1},${2} +${3},${4} @@"));
+
+        # Generate a new set of line numbers and patch lengths.  Our new
+        # patch will start with the lines for the fixed ChangeLog entry,
+        # then have 3 lines of context from the top of the current file to
+        # make the patch apply cleanly.
+        $newPatch .= "@@ -1,3 +1," . ($4 - $2 + 3) . " @@\n";
+
+        # We assume that top few lines of the ChangeLog entry are actually
+        # at the bottom of the list of added lines (due to the way the patch
+        # algorithm works), so we simply search through the lines until we
+        # find the date line, then move the rest of the lines to the top.
+        my @patchLines = map { $_ . "\n" } split(/\n/, $6);
+        foreach my $i (0 .. $#patchLines) {
+            if ($patchLines[$i] =~ /^\+\d{4}-\d{2}-\d{2}  /) {
+                unshift(@patchLines, splice(@patchLines, $i, scalar(@patchLines) - $i));
+                last;
+            }
+        }
+
+        $newPatch .= join("", @patchLines);
+
+        # Add 3 lines of context to the end
+        open FILE, "<", $file or die $!;
+        for (my $i = 0; $i < 3; $i++) {
+            $newPatch .= " " . <FILE>;
+        }
+        close FILE;
+
+        # Apply the new patch
+        open(PATCH, "| patch -p1 $file > " . File::Spec->devnull()) or die $!;
+        print PATCH $newPatch;
+        close(PATCH) or die $!;
+
+        # Run "git add" on the fixed ChangeLog file
+        system($GIT, "add", $file);
+        die $! if WEXITSTATUS($?);
+
+        showStatus($file, 1);
+    } elsif ($patch) {
+        # Restore the current copy of the ChangeLog file since we can't repatch it
+        system($GIT, "checkout", $ENV{GIT_COMMIT} || "HEAD", $file);
+        die $! if WEXITSTATUS($?);
+        print STDERR "WARNING: Last change to ${file} could not be fixed and re-merged.\n" if $printWarnings;
+    }
+}
+
+sub hasGitUnmergedFiles()
+{
+    my $output = `$GIT ls-files --unmerged`;
+    return $output ne "";
+}
+
+sub isInGitFilterBranch()
+{
+    return exists $ENV{MAPPED_PREVIOUS_COMMIT} && $ENV{MAPPED_PREVIOUS_COMMIT};
+}
+
+sub parseFixMerged($$;$)
+{
+    my ($switchName, $key, $value) = @_;
+    if (defined $key) {
+        if (defined findChangeLog($key)) {
+            unshift(@ARGV, $key);
+            $fixMerged = "";
+        } else {
+            $fixMerged = $key;
+        }
+    } else {
+        $fixMerged = "";
+    }
+}
+
+sub removeChangeLogArguments($)
+{
+    my ($baseDir) = @_;
+    my @results = ();
+
+    for (my $i = 0; $i < scalar(@ARGV); ) {
+        my $file = findChangeLog(canonicalRelativePath(File::Spec->catfile($baseDir, $ARGV[$i])));
+        if (defined $file) {
+            splice(@ARGV, $i, 1);
+            push @results, $file;
+        } else {
+            $i++;
+        }
+    }
+
+    return @results;
+}
+
+sub resolveChangeLog($)
+{
+    my ($file) = @_;
+
+    my ($fileMine, $fileOlder, $fileNewer) = conflictFiles($file);
+
+    return unless $fileMine && $fileOlder && $fileNewer;
+
+    if (mergeChangeLogs($fileMine, $fileOlder, $fileNewer)) {
+        if ($file ne $fileNewer) {
+            unlink($file);
+            rename($fileNewer, $file) or die $!;
+        }
+        unlink($fileMine, $fileOlder);
+        resolveConflict($file);
+        showStatus($file, 1);
+    } else {
+        showStatus($file);
+        print STDERR "WARNING: ${file} could not be merged using fuzz level 3.\n" if $printWarnings;
+        unlink($fileMine, $fileOlder, $fileNewer) if $isGit;
+    }
+}
+
+sub resolveConflict($)
+{
+    my ($file) = @_;
+
+    if ($isSVN) {
+        my $escapedFile = escapeSubversionPath($file);
+        system($SVN, "resolved", $escapedFile);
+        die $! if WEXITSTATUS($?);
+    } elsif ($isGit) {
+        system($GIT, "add", $file);
+        die $! if WEXITSTATUS($?);
+    } else {
+        die "Unknown version control system";
+    }
+}
+
+sub showStatus($;$)
+{
+    my ($file, $isConflictResolved) = @_;
+
+    if ($isSVN) {
+        my $escapedFile = escapeSubversionPath($file);
+        system($SVN, "status", $escapedFile);
+    } elsif ($isGit) {
+        my @args = qw(--name-status);
+        unshift @args, qw(--cached) if $isConflictResolved;
+        system($GIT, "diff", @args, $file);
+    } else {
+        die "Unknown version control system";
+    }
+}
+
diff --git a/Tools/Scripts/roll-over-ChangeLogs b/Tools/Scripts/roll-over-ChangeLogs
new file mode 100755
index 0000000..7e6d32f
--- /dev/null
+++ b/Tools/Scripts/roll-over-ChangeLogs
@@ -0,0 +1,47 @@
+#!/usr/bin/env ruby
+
+# Copyright (C) 2009 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. ``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
+# 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. 
+
+
+require 'date'
+
+CHANGELOG_SIZE_THRESHOLD = 750 * 1024
+
+scripts_directory = File.dirname(__FILE__)
+base_directory = File.expand_path(ARGV[0] || `perl -I#{scripts_directory} -Mwebkitdirs -e 'print sourceDir();'`)
+
+date_suffix = Date.today.strftime("-%Y-%m-%d")
+
+Dir.chdir base_directory
+`find . -type f -name 'ChangeLog'`.split.each do |path|
+    next unless File.stat(path).size > CHANGELOG_SIZE_THRESHOLD
+
+    old_path = "#{path}#{date_suffix}"
+    puts "Moving #{path} to #{old_path}..."
+    system "git", "mv", path, old_path
+    File.open path, "w" do |file|
+        file.write "== Rolled over to ChangeLog#{date_suffix} ==\n"
+    end
+    system "git", "add", path
+end
diff --git a/Tools/Scripts/run-api-tests b/Tools/Scripts/run-api-tests
new file mode 100755
index 0000000..3a3e00c
--- /dev/null
+++ b/Tools/Scripts/run-api-tests
@@ -0,0 +1,332 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2010, 2011, 2012 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 warnings;
+
+use File::Basename;
+use FindBin;
+use Getopt::Long qw(:config pass_through);
+use IPC::Open3;
+use lib $FindBin::Bin;
+use webkitdirs;
+use VCSUtils;
+
+sub buildTestTool();
+sub dumpTestsBySuite(\@);
+sub listAllTests();
+sub runTest($$$);
+sub runTestsBySuite(\@$);
+sub prepareEnvironmentForRunningTestTool();
+sub testToolPath();
+
+# Defined in VCSUtils.
+sub possiblyColored($$);
+
+# Timeout for individual test, in sec
+my $timeout = 10;
+
+my $showHelp = 0;
+my $verbose = 0;
+my $dumpTests = 0;
+my $build = 1;
+my $buildDefault = $build ? "build" : "do not build";
+my @testsFailed;
+my @testsTimedOut;
+
+my $programName = basename($0);
+my $usage = <<EOF;
+Usage: $programName [options] [suite or test prefixes]
+  --help                Show this help message
+  -v|--verbose          Verbose output
+  -d|--dump-tests       Dump the names of testcases without running them
+  --[no-]build          Build (or do not build) unit tests prior to running (default: $buildDefault)
+  --chromium            Run the Chromium port on Mac/Win/Linux
+EOF
+
+GetOptions(
+    'help' => \$showHelp,
+    'verbose|v' => \$verbose,
+    'dump|d' => \$dumpTests,
+    'build!' => \$build
+);
+
+if ($showHelp) {
+   print STDERR $usage;
+   exit 1;
+}
+
+setConfiguration();
+buildTestTool() if $build;
+setPathForRunningWebKitApp(\%ENV);
+my @testsToRun = listAllTests();
+
+@testsToRun = grep { my $test = $_; grep { $test =~ m/^\Q$_\E/ } @ARGV; } @testsToRun if @ARGV;
+
+if ($dumpTests) {
+    dumpTestsBySuite(@testsToRun);
+    exit 0;
+}
+
+exit runTestsBySuite(@testsToRun, $verbose);
+
+sub isSupportedPlatform()
+{
+    return isAppleMacWebKit() || isAppleWinWebKit() || isChromium();
+}
+
+sub dumpTestsBySuite(\@)
+{
+    my ($tests) = @_;
+    print "Dumping test cases\n";
+    print "------------------\n";
+    my $lastSuite = "";
+    for my $suiteAndTest (sort @$tests) {
+        my ($suite, $test) = split(/\./, $suiteAndTest);
+        if ($lastSuite ne $suite) {
+            $lastSuite = $suite;
+            print "$suite:\n";
+        }
+        print "   $test\n";
+    }
+    print "------------------\n";
+}
+
+sub runTestsBySuite(\@$)
+{
+    my ($tests, $verbose) = @_;
+    my $anyFailures = 0;
+    my $lastSuite = "";
+    for my $suiteAndTest (sort @$tests) {
+        my ($suite, $test) = split(/\./, $suiteAndTest);
+        if ($lastSuite ne $suite) {
+            $lastSuite = $suite;
+            print "Suite: $suite\n" unless $verbose;
+        }
+        my $failed = runTest($suite, $test, $verbose);
+        $anyFailures ||= $failed;
+    }
+    
+    if ($verbose) {
+        if (@testsFailed) {
+            print "Tests that failed:\n";
+            for my $test (@testsFailed) {
+                print "  $test\n";
+            }
+        }
+        if (@testsTimedOut) {
+            print "Tests that timed out:\n";
+            for my $test (@testsTimedOut) {
+                print "  $test\n";
+            }
+        }
+    }
+    return $anyFailures;
+}
+
+sub runTest($$$)
+{
+    my ($suite, $testName, $verbose) = @_;
+    my $test = $suite . "." . $testName;
+
+    my $gtestArg = "--gtest_filter=" . $test;
+
+    print "    Test: $testName -> " unless $verbose;
+
+    my $result = 0;
+    my $timedOut = 0;
+
+    die "run-api-tests is not supported on this platform.\n" unless isSupportedPlatform();
+
+    prepareEnvironmentForRunningTestTool();
+
+    local *DEVNULL;
+    my ($childIn, $childOut, $childErr);
+    if ($verbose) {
+        $childOut = ">&STDERR";
+        $childErr = ">&STDERR";
+    } else {
+        open(DEVNULL, ">", File::Spec->devnull()) or die "Failed to open /dev/null";
+        $childOut = ">&DEVNULL";
+        $childErr = ">&DEVNULL";
+    }
+
+    my $pid;
+    if (isAppleMacWebKit() && architecture()) {
+        $pid = open3($childIn, $childOut, $childErr, "arch", "-" . architecture(), testToolPath(), $gtestArg, @ARGV) or die "Failed to run test: $test.";
+    } else {
+        $pid = open3($childIn, $childOut, $childErr, testToolPath(), $gtestArg, @ARGV) or die "Failed to run test: $test.";
+    }
+
+    close($childIn);
+    close($childOut);
+    close($childErr);
+    close(DEVNULL) unless ($verbose);
+    eval {
+        local $SIG{ALRM} = sub { die "alarm\n" };
+        alarm $timeout;
+        waitpid($pid, 0);
+        alarm 0;
+        $result = $?;
+    };
+    if ($@) {
+        die unless $@ eq "alarm\n";
+        kill SIGTERM, $pid or kill SIGKILL, $pid;
+        $timedOut = 1;
+    }
+
+    if ($result) {
+        push @testsFailed, $test;
+    }
+    if ($timedOut) {
+        push @testsTimedOut, $test;
+        print possiblyColored("bold yellow", "Timeout"), "\n";
+    } elsif (!$verbose) {
+        if ($result) {
+            print possiblyColored("bold red", "Failed"), "\n";
+        } else {
+            print possiblyColored("bold green", "Passed"), "\n";
+        }
+    }
+
+    return $timedOut || $result;
+}
+
+sub listAllTests()
+{
+    my @toolOutput;
+    my $timedOut;
+
+    die "run-api-tests is not supported on this platform.\n" unless isSupportedPlatform();
+
+    prepareEnvironmentForRunningTestTool();
+
+    local *DEVNULL;
+    my ($childIn, $childOut, $childErr);
+    if ($verbose) {
+        $childErr = ">&STDERR";
+    } else {
+        open(DEVNULL, ">", File::Spec->devnull()) or die "Failed to open /dev/null";
+        $childErr = ">&DEVNULL";
+    }
+
+    my $pid;
+    if (isAppleMacWebKit() && architecture()) {
+        $pid = open3($childIn, $childOut, $childErr, "arch", "-" . architecture(), testToolPath(), "--gtest_list_tests") or die "Failed to build list of tests!";
+    } else {
+        $pid = open3($childIn, $childOut, $childErr, testToolPath(), "--gtest_list_tests") or die "Failed to build list of tests!";
+    }
+
+    close($childIn);
+    @toolOutput = <$childOut>;
+    close($childOut);
+    close($childErr);
+    close(DEVNULL) unless ($verbose);
+
+    waitpid($pid, 0);
+    my $result = $?;
+
+    if ($result) {
+        print STDERR "Failed to build list of tests!\n";
+        exit exitStatus($result);
+    }
+
+    my @tests = ();
+    my $suite;
+    for my $line (@toolOutput) {
+       $line =~ s/[\r\n]*$//;
+       if ($line =~ m/\.$/) {
+          $suite = $line; # "SuiteName."
+       } else {
+          $line =~ s/^\s*//; # "TestName"
+          push @tests, $suite . $line; # "SuiteName.TestName"
+        }
+    }
+
+    return @tests;
+}
+
+sub buildTestTool()
+{
+    my $originalCwd = getcwd();
+
+    chdirWebKit();
+
+    my $buildTestTool = "build-api-tests";
+    print STDERR "Running $buildTestTool\n";
+
+    local *DEVNULL;
+    my ($childIn, $childOut, $childErr);
+    if ($verbose) {
+        # When not quiet, let the child use our stdout/stderr.
+        $childOut = ">&STDOUT";
+        $childErr = ">&STDERR";
+    } else {
+        open(DEVNULL, ">", File::Spec->devnull()) or die "Failed to open /dev/null";
+        $childOut = ">&DEVNULL";
+        $childErr = ">&DEVNULL";
+    }
+
+    my @args = argumentsForConfiguration();
+    my $pathToBuildTestTool = File::Spec->catfile("Tools", "Scripts", $buildTestTool);
+    my $buildProcess = open3($childIn, $childOut, $childErr, "perl", $pathToBuildTestTool, @args) or die "Failed to run " . $buildTestTool;
+
+    close($childIn);
+    close($childOut);
+    close($childErr);
+    close(DEVNULL) unless ($verbose);
+
+    waitpid($buildProcess, 0);
+    my $buildResult = $?;
+
+    if ($buildResult) {
+        print STDERR "Compiling TestWebKitAPI failed!\n";
+        exit exitStatus($buildResult);
+    }
+
+    chdir $originalCwd;
+}
+
+sub prepareEnvironmentForRunningTestTool()
+{
+    return unless isAppleMacWebKit();
+
+    $ENV{DYLD_FRAMEWORK_PATH} = productDir();
+    $ENV{WEBKIT_UNSET_DYLD_FRAMEWORK_PATH} = "YES";
+}
+
+sub testToolPath()
+{
+    my $path = File::Spec->catfile(productDir(), "TestWebKitAPI");
+    return $path unless isAppleWinWebKit();
+
+    my $suffix;
+    if (configurationForVisualStudio() eq "Debug_All") {
+        $suffix = "_debug";
+    } else {
+        $suffix = "";
+    }
+    return "$path$suffix.exe";
+}
diff --git a/Tools/Scripts/run-bindings-tests b/Tools/Scripts/run-bindings-tests
new file mode 100755
index 0000000..a0785e4
--- /dev/null
+++ b/Tools/Scripts/run-bindings-tests
@@ -0,0 +1,58 @@
+#!/usr/bin/python
+# Copyright (C) 2010 Google Inc.  All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 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 COMPUTER, INC. ``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 COMPUTER, INC. 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 script generates h and cpp file for TestObj.idl using code
+# generators. Please execute the script whenever changes are made to
+# CodeGeneratorXXXX.pm, and submit the changes in XXXXTestObj.h/cpp in the same
+# patch. This makes it easier to track and review changes in generated code.
+
+import sys
+from webkitpy.common.system import executive
+
+def main(argv):
+    """Runs WebCore bindings code generators on test IDL files and compares
+    the results with reference files.
+
+    Options:
+       --reset-results: Overwrites the reference files with the generated results.
+
+    """
+    reset_results = "--reset-results" in argv
+
+    generators = [
+        'JS',
+        'V8',
+        'ObjC',
+        'GObject',
+        'CPP'
+    ]
+
+    from webkitpy.bindings.main import BindingsTests
+
+    return BindingsTests(reset_results, generators, executive.Executive()).main()
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))
diff --git a/Tools/Scripts/run-chromium-webkit-unit-tests b/Tools/Scripts/run-chromium-webkit-unit-tests
new file mode 100755
index 0000000..43f85ad
--- /dev/null
+++ b/Tools/Scripts/run-chromium-webkit-unit-tests
@@ -0,0 +1,40 @@
+#!/usr/bin/perl -w
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+use strict;
+use File::Spec;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+use VCSUtils;
+
+push(@ARGV, "--chromium");
+setConfiguration();
+my $pathToBinary = productDir() . "/webkit_unit_tests";
+exit exitStatus(system ($pathToBinary, @ARGV));
diff --git a/Tools/Scripts/run-efl-tests b/Tools/Scripts/run-efl-tests
new file mode 100755
index 0000000..52d119a
--- /dev/null
+++ b/Tools/Scripts/run-efl-tests
@@ -0,0 +1,61 @@
+#!/usr/bin/perl -w
+# Copyright (C) 2012 Intel Corporation. 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 Intel Corporation nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+use strict;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+my $xvfb_display = ":55";
+
+my $xvfb_pid = fork();
+exit(1) if not defined $xvfb_pid;
+
+# Tell CTest to dump gtest output in case of failure.
+$ENV{CTEST_OUTPUT_ON_FAILURE} = "1";
+$ENV{DISPLAY} = $xvfb_display;
+
+if ($xvfb_pid == 0) {
+    # Start Xvfb
+    my @xvfb_args = ( "Xvfb $xvfb_display -screen 0 800x600x24 -nolisten tcp > /dev/null 2>&1" );
+    exec(@xvfb_args);
+} else {
+    setConfiguration();
+
+    my $returnCode = exitStatus(generateBuildSystemFromCMakeProject("Efl", undef, cmakeBasedPortArguments()));
+    exit($returnCode) if $returnCode;
+
+    $returnCode = exitStatus(buildCMakeGeneratedProject("test"));
+
+    # Kill Xvfb
+    kill(15, $xvfb_pid);
+
+    exit($returnCode);
+}
+
diff --git a/Tools/Scripts/run-fast-jsc b/Tools/Scripts/run-fast-jsc
new file mode 100755
index 0000000..8e004c4
--- /dev/null
+++ b/Tools/Scripts/run-fast-jsc
@@ -0,0 +1,161 @@
+#! /bin/sh
+
+# Copyright (C) 2012 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. ``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
+# 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 run selected LayoutTests/fast/{js,regex} tests using jsc
+
+jscCmd="/System/Library/Frameworks/JavaScriptCore.framework/Resources/jsc"
+testRoot=/tmp/LayoutTests
+resultsRoot=`date \+/tmp/results-%Y-%m-%d-%H-%M-%S`
+testList="unset"
+
+cmdName=`basename $0`
+
+function usage()
+{
+    echo "usage: $cmdName [[--jsc | -j] <path-to-jsc>] [[--results-dir | -r] <results-path>]"
+    echo "                    [[--test-root | -t] <test-root-path>] [[--test-list | -l] <test-list-file>]"
+    exit 1
+}
+
+while [ $# -gt 1 ]
+do
+    case $1 in
+    --jsc|-j)
+        jscCmd=$2
+        ;;
+    --results-dir|-r)
+        resultsRoot=$2
+        ;;
+    --test-root|-t)
+        testRoot=$2
+        ;;
+    --test-list|-l)
+        testList=$2
+        ;;
+    *)
+        echo "Unrecognized option \"$1\""
+        usage
+        ;;
+    esac
+
+    shift 2
+done
+
+if [ $# -gt 0 ]
+then
+    echo "Extra argument \"$1\""
+    usage
+fi
+
+if [ $testList = "unset" ]
+then
+    testList=$testRoot/fast/js/jsc-test-list
+fi
+
+preScript=$testRoot/fast/js/resources/standalone-pre.js
+postScript=$testRoot/fast/js/resources/standalone-post.js
+passList=$resultsRoot/passed
+failList=$resultsRoot/failed
+crashList=$resultsRoot/crashed
+
+numTestsRun=0
+numPassed=0
+numFailed=0
+numCrashed=0
+
+rm -rf $resultsRoot
+rm -f jsc-tests-passed jsc-tests-failed
+
+for test in `cat $testList`
+do
+    testPassed=0
+    testCrashed=0
+    testName=`basename $test`
+    dirName=`dirname $test`
+     
+    expectedOut="$testRoot/$dirName/${testName}-expected.txt"
+    actualOut="$resultsRoot/$dirName/${testName}-out.txt"
+    actualErr="$resultsRoot/$dirName/${testName}-err.txt"
+    diffOut="$resultsRoot/$dirName/${testName}-diff.txt"
+    jsTest="$testRoot/$dirName/script-tests/${testName}.js"
+
+    if [ ! -d "$resultsRoot/$dirName" ]
+    then
+        mkdir -p "$resultsRoot/$dirName"
+    fi
+
+    if [ -f $expectedOut -a -f $jsTest ]
+    then
+        echo "Testing $test ... \c"
+        let numTestsRun=$numTestsRun+1
+        $jscCmd $preScript $jsTest $postScript 2>$actualErr > $actualOut
+        JSC_RES=$?
+        
+        if [ $JSC_RES -eq 0 ]
+        then
+            diff $actualOut $expectedOut > $diffOut
+            if [ $? -eq 0 ]
+            then
+                testPassed=1
+                echo "PASSED"
+            else
+                testPassed=0
+                echo "FAILED"
+            fi
+        else
+            testPassed=0
+            if [ $JSC_RES -gt 128 ]
+            then
+                testCrashed=1
+                echo "CRASHED"
+            else
+                echo "ERROR: $JSC_RES"
+            fi
+        fi
+
+        if [ $testPassed -eq 1 ]
+        then
+            echo "$test" >> $passList
+            let numPassed=$numPassed+1
+        else
+            echo "$test" >> $failList
+            let numFailed=$numFailed+1
+            if [ $testCrashed -eq 1 ]
+            then
+                echo "$test" >> $crashList
+                let numCrashed=$numCrashed+1
+            fi
+        fi
+    fi
+done
+
+if [ $numPassed -eq $numTestsRun ]
+then
+    echo "All $numTestsRun tests passed!" | tee $resultsRoot/summary
+else
+    echo "$numPassed tests passed, $numFailed tests failed, $numCrashed tests crashed." | tee $resultsRoot/summary
+fi
+
+echo "Test results in $resultsRoot"
diff --git a/Tools/Scripts/run-gtk-tests b/Tools/Scripts/run-gtk-tests
new file mode 100755
index 0000000..b24cb24
--- /dev/null
+++ b/Tools/Scripts/run-gtk-tests
@@ -0,0 +1,377 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2011, 2012 Igalia S.L.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Library General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Library General Public License for more details.
+#
+# You should have received a copy of the GNU Library General Public License
+# along with this library; see the file COPYING.LIB.  If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+
+import subprocess
+import os
+import sys
+import optparse
+import re
+from signal import alarm, signal, SIGALRM, SIGKILL
+from gi.repository import Gio, GLib
+
+top_level_directory = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
+sys.path.append(os.path.join(top_level_directory, "Tools", "jhbuild"))
+sys.path.append(os.path.join(top_level_directory, "Tools", "gtk"))
+import common
+import jhbuildutils
+
+class SkippedTest:
+    ENTIRE_SUITE = None
+
+    def __init__(self, test, test_case, reason, bug=None):
+        self.test = test
+        self.test_case = test_case
+        self.reason = reason
+        self.bug = bug
+
+    def __str__(self):
+        skipped_test_str = "%s" % self.test
+
+        if not(self.skip_entire_suite()):
+            skipped_test_str += " [%s]" % self.test_case
+
+        skipped_test_str += ": %s " % self.reason
+        if self.bug is not None:
+            skipped_test_str += "(https://bugs.webkit.org/show_bug.cgi?id=%d)" % self.bug
+        return skipped_test_str
+
+    def skip_entire_suite(self):
+        return self.test_case == SkippedTest.ENTIRE_SUITE
+
+class TestTimeout(Exception):
+    pass
+
+class TestRunner:
+    TEST_DIRS = [ "unittests", "WebKit2APITests", "TestWebKitAPI" ]
+
+    SKIPPED = [
+        SkippedTest("unittests/testdownload", "/webkit/download/not-found", "Test fails in GTK Linux 64-bit Release bot", 82329),
+        SkippedTest("unittests/testwebview", "/webkit/webview/icon-uri", "Test times out in GTK Linux 64-bit Release bot", 82328),
+        SkippedTest("unittests/testwebresource", "/webkit/webresource/sub_resource_loading", "Test fails in GTK Linux 64-bit Release bot", 82330),
+        SkippedTest("unittests/testwebinspector", "/webkit/webinspector/close-and-inspect", "Test is flaky in GTK Linux 32-bit Release bot", 82869),
+        SkippedTest("WebKit2APITests/TestWebKitWebView", "/webkit2/WebKitWebView/mouse-target", "Test is flaky in GTK Linux 32-bit Release bot", 82866),
+        SkippedTest("WebKit2APITests/TestResources", "/webkit2/WebKitWebView/resources", "Test is flaky in GTK Linux 32-bit Release bot", 82868),
+        SkippedTest("WebKit2APITests/TestWebKitFindController", "/webkit2/WebKitFindController/hide", "Test always fails in Xvfb", 89810),
+        SkippedTest("WebKit2APITests/TestWebKitAccessibility", "/webkit2/WebKitAccessibility/atspi-basic-hierarchy", "Test fails", 100408),
+        SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.WKConnection", "Tests fail and time out out", 84959),
+        SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.RestoreSessionStateContainingFormData", "Session State is not implemented in GTK+ port", 84960),
+        SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.SpacebarScrolling", "Test fails", 84961),
+        SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.NewFirstVisuallyNonEmptyLayoutForImages", "Test is flaky", 85066),
+        SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.NewFirstVisuallyNonEmptyLayoutFrames", "Test fails", 85037),
+        SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.MouseMoveAfterCrash", "Test is flaky", 85066),
+        SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.CanHandleRequest", "Test fails", 88453),
+        SkippedTest("TestWebKitAPI/TestWebKit2", "WebKit2.WKPageGetScaleFactorNotZero", "Test fails and times out", 88455),
+    ]
+
+    def __init__(self, options, tests=[]):
+        self._options = options
+        self._programs_path = common.build_path("Programs")
+        self._tests = self._get_tests(tests)
+        self._skipped_tests = TestRunner.SKIPPED
+        if not sys.stdout.isatty():
+            self._tty_colors_pattern = re.compile("\033\[[0-9;]*m")
+
+        # These SPI daemons need to be active for the accessibility tests to work.
+        self._spi_registryd = None
+        self._spi_bus_launcher = None
+
+    def _get_tests(self, tests):
+        if tests:
+            return tests
+
+        tests = []
+        for test_dir in self.TEST_DIRS:
+            absolute_test_dir = os.path.join(self._programs_path, test_dir)
+            if not os.path.isdir(absolute_test_dir):
+                continue
+            for test_file in os.listdir(absolute_test_dir):
+                if not test_file.lower().startswith("test"):
+                    continue
+                test_path = os.path.join(self._programs_path, test_dir, test_file)
+                if os.path.isfile(test_path) and os.access(test_path, os.X_OK):
+                    tests.append(test_path)
+        return tests
+
+    def _lookup_atspi2_binary(self, filename):
+        exec_prefix = common.pkg_config_file_variable('atspi-2', 'exec_prefix')
+        if not exec_prefix:
+            return None
+        for path in ['libexec', 'lib/at-spi2-core', 'lib32/at-spi2-core', 'lib64/at-spi2-core']:
+            filepath = os.path.join(exec_prefix, path, filename)
+            if os.path.isfile(filepath):
+                return filepath
+
+        return None
+
+    def _start_accessibility_daemons(self):
+        spi_bus_launcher_path = self._lookup_atspi2_binary('at-spi-bus-launcher')
+        spi_registryd_path = self._lookup_atspi2_binary('at-spi2-registryd')
+        if not spi_bus_launcher_path or not spi_registryd_path:
+            return False
+
+        try:
+            self._ally_bus_launcher = subprocess.Popen([spi_bus_launcher_path], env=self._test_env)
+        except:
+            sys.stderr.write("Failed to launch the accessibility bus\n")
+            sys.stderr.flush()
+            return False
+
+        # We need to wait until the SPI bus is launched before trying to start the SPI
+        # registry, so we spin a main loop until the bus name appears on DBus.
+        loop = GLib.MainLoop()
+        Gio.bus_watch_name(Gio.BusType.SESSION, 'org.a11y.Bus', Gio.BusNameWatcherFlags.NONE,
+                           lambda *args: loop.quit(), None)
+        loop.run()
+
+        try:
+            self._spi_registryd = subprocess.Popen([spi_registryd_path], env=self._test_env)
+        except:
+            sys.stderr.write("Failed to launch the accessibility registry\n")
+            sys.stderr.flush()
+            return False
+
+        return True
+
+    def _setup_testing_environment(self):
+        self._test_env = os.environ
+        self._test_env["DISPLAY"] = self._options.display
+        self._test_env["WEBKIT_INSPECTOR_PATH"] = os.path.abspath(os.path.join(self._programs_path, 'resources', 'inspector'))
+        self._test_env['GSETTINGS_BACKEND'] = 'memory'
+        self._test_env["TEST_WEBKIT_API_WEBKIT2_RESOURCES_PATH"] = common.top_level_path("Tools", "TestWebKitAPI", "Tests", "WebKit2")
+        self._test_env["TEST_WEBKIT_API_WEBKIT2_INJECTED_BUNDLE_PATH"] = common.build_path("Libraries")
+        self._test_env["WEBKIT_EXEC_PATH"] = self._programs_path
+
+        try:
+            self._xvfb = subprocess.Popen(["Xvfb", self._options.display, "-screen", "0", "800x600x24", "-nolisten", "tcp"],
+                                          stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        except Exception as e:
+            sys.stderr.write("Failed to run Xvfb: %s\n" % e)
+            sys.stderr.flush()
+            return False
+
+        # If we cannot start the accessibility daemons, we can just skip the accessibility tests.
+        if not self._start_accessibility_daemons():
+            print "Could not start accessibility bus, so skipping TestWebKitAccessibility"
+            self._skipped_tests.append(SkippedTest("WebKit2APITests/TestWebKitAccessibility", SkippedTest.ENTIRE_SUITE, "Could not start accessibility bus"))
+        return True
+
+    def _tear_down_testing_environment(self):
+        if self._spi_registryd:
+            self._spi_registryd.terminate()
+        if self._spi_bus_launcher:
+            self._spi_bus_launcher.terminate()
+        self._xvfb.terminate()
+
+    def _test_cases_to_skip(self, test_program):
+        if self._options.skipped_action != 'skip':
+            return []
+
+        test_cases = []
+        for skipped in self._skipped_tests:
+            if test_program.endswith(skipped.test) and not skipped.skip_entire_suite():
+                test_cases.append(skipped.test_case)
+        return test_cases
+
+    def _should_run_test_program(self, test_program):
+        # This is not affected by the command-line arguments, since programs are skipped for
+        # problems in the harness, such as failing to start the accessibility bus.
+        for skipped in self._skipped_tests:
+            if test_program.endswith(skipped.test) and skipped.skip_entire_suite():
+                return False
+        return True
+
+    def _get_child_pid_from_test_output(self, output):
+        if not output:
+            return -1
+        match = re.search(r'\(pid=(?P<child_pid>[0-9]+)\)', output)
+        if not match:
+            return -1
+        return int(match.group('child_pid'))
+
+    def _kill_process(self, pid):
+        try:
+            os.kill(pid, SIGKILL)
+        except OSError:
+            # Process already died.
+            pass
+
+    def _run_test_command(self, command, timeout=-1):
+        def alarm_handler(signum, frame):
+            raise TestTimeout
+
+        child_pid = [-1]
+        def parse_line(line, child_pid = child_pid):
+            if child_pid[0] == -1:
+                child_pid[0] = self._get_child_pid_from_test_output(line)
+
+            if sys.stdout.isatty():
+                sys.stdout.write(line)
+            else:
+                sys.stdout.write(self._tty_colors_pattern.sub('', line.replace('\r', '')))
+
+        def waitpid(pid):
+            while True:
+                try:
+                    return os.waitpid(pid, 0)
+                except (OSError, IOError) as e:
+                    if e.errno == errno.EINTR:
+                        continue
+                    raise
+
+        def return_code_from_exit_status(status):
+            if os.WIFSIGNALED(status):
+                return -os.WTERMSIG(status)
+            elif os.WIFEXITED(status):
+                return os.WEXITSTATUS(status)
+            else:
+                # Should never happen
+                raise RuntimeError("Unknown child exit status!")
+
+        pid, fd = os.forkpty()
+        if pid == 0:
+            os.execvpe(command[0], command, self._test_env)
+            sys.exit(0)
+
+        if timeout > 0:
+            signal(SIGALRM, alarm_handler)
+            alarm(timeout)
+
+        try:
+            common.parse_output_lines(fd, parse_line)
+            if timeout > 0:
+                alarm(0)
+        except TestTimeout:
+            self._kill_process(pid)
+            if child_pid[0] > 0:
+                self._kill_process(child_pid[0])
+            raise
+
+        try:
+            dummy, status = waitpid(pid)
+        except OSError as e:
+            if e.errno != errno.ECHILD:
+                raise
+            # This happens if SIGCLD is set to be ignored or waiting
+            # for child processes has otherwise been disabled for our
+            # process.  This child is dead, we can't get the status.
+            status = 0
+
+        return not return_code_from_exit_status(status)
+
+    def _run_test_glib(self, test_program):
+        tester_command = ['gtester']
+        if self._options.verbose:
+            tester_command.append('--verbose')
+        for test_case in self._test_cases_to_skip(test_program):
+            tester_command.extend(['-s', test_case])
+        tester_command.append(test_program)
+
+        return self._run_test_command(tester_command, self._options.timeout)
+
+    def _run_test_google(self, test_program):
+        tester_command = [test_program]
+        skipped_tests_cases = self._test_cases_to_skip(test_program)
+        if skipped_tests_cases:
+            tester_command.append("--gtest_filter=-%s" % ":".join(skipped_tests_cases))
+
+        return self._run_test_command(tester_command, self._options.timeout)
+
+    def _run_test(self, test_program):
+        if "unittests" in test_program or "WebKit2APITests" in test_program:
+            return self._run_test_glib(test_program)
+
+        if "TestWebKitAPI" in test_program:
+            return self._run_test_google(test_program)
+
+        return False
+
+    def run_tests(self):
+        if not self._tests:
+            sys.stderr.write("ERROR: tests not found in %s.\n" % (self._programs_path))
+            sys.stderr.flush()
+            return 1
+
+        if not self._setup_testing_environment():
+            return 1
+
+        # Remove skipped tests now instead of when we find them, because
+        # some tests might be skipped while setting up the test environment.
+        self._tests = [test for test in self._tests if self._should_run_test_program(test)]
+
+        failed_tests = []
+        timed_out_tests = []
+        try:
+            for test in self._tests:
+                success = True
+                try:
+                    success = self._run_test(test)
+                except TestTimeout:
+                    sys.stdout.write("TEST: %s: TIMEOUT\n" % test)
+                    sys.stdout.flush()
+                    timed_out_tests.append(test)
+
+                if not success:
+                    failed_tests.append(test)
+        finally:
+            self._tear_down_testing_environment()
+
+        if failed_tests:
+            names = [test.replace(self._programs_path, '', 1) for test in failed_tests]
+            sys.stdout.write("Tests failed: %s\n" % ", ".join(names))
+            sys.stdout.flush()
+
+        if timed_out_tests:
+            names = [test.replace(self._programs_path, '', 1) for test in timed_out_tests]
+            sys.stdout.write("Tests that timed out: %s\n" % ", ".join(names))
+            sys.stdout.flush()
+
+        if self._skipped_tests and self._options.skipped_action == 'skip':
+            sys.stdout.write("Tests skipped:\n%s\n" % "\n".join([str(skipped) for skipped in self._skipped_tests]))
+            sys.stdout.flush()
+
+        return len(failed_tests)
+
+if __name__ == "__main__":
+    if not jhbuildutils.enter_jhbuild_environment_if_available("gtk"):
+        print "***"
+        print "*** Warning: jhbuild environment not present. Run update-webkitgtk-libs before build-webkit to ensure proper testing."
+        print "***"
+
+    option_parser = optparse.OptionParser(usage='usage: %prog [options] [test...]')
+    option_parser.add_option('-r', '--release',
+                             action='store_true', dest='release',
+                             help='Run in Release')
+    option_parser.add_option('-d', '--debug',
+                             action='store_true', dest='debug',
+                             help='Run in Debug')
+    option_parser.add_option('-v', '--verbose',
+                             action='store_true', dest='verbose',
+                             help='Run gtester in verbose mode')
+    option_parser.add_option('--display', action='store', dest='display', default=':55',
+                             help='Display to run Xvfb')
+    option_parser.add_option('--skipped', action='store', dest='skipped_action',
+                             choices=['skip', 'ignore', 'only'], default='skip',
+                             metavar='skip|ignore|only',
+                             help='Specifies how to treat the skipped tests')
+    option_parser.add_option('-t', '--timeout',
+                             action='store', type='int', dest='timeout', default=10,
+                             help='Time in seconds until a test times out')
+    options, args = option_parser.parse_args()
+
+    sys.exit(TestRunner(options, args).run_tests())
diff --git a/Tools/Scripts/run-iexploder-tests b/Tools/Scripts/run-iexploder-tests
new file mode 100755
index 0000000..cb696a2
--- /dev/null
+++ b/Tools/Scripts/run-iexploder-tests
@@ -0,0 +1,142 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2006 Alexey Proskuryakov (ap@nypop.com)
+#
+# 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.
+
+# A script to semi-automatically run iExploder tests.
+
+use strict;
+use warnings;
+
+use Cwd;
+use File::Spec;
+use FindBin;
+use Getopt::Long;
+use IPC::Open2;
+
+use lib $FindBin::Bin;
+use webkitperl::httpd;
+use webkitdirs;
+
+sub configureAndOpenHTTPDIfNeeded();
+sub runSafariWithIExploder();
+
+# Argument handling
+my $guardMalloc = '';
+my $httpdPort = 8000;
+my $downloadTest;
+my $iExploderTestDirectory = "/tmp/iExploderTest";
+
+GetOptions(
+    'guard-malloc|g' => \$guardMalloc,
+    'get=s' => \$downloadTest,
+    'port=i' => \$httpdPort
+);
+
+
+setConfiguration();
+my $productDir = productDir();
+
+chdirWebKit();
+
+checkFrameworks();
+
+my $isHttpdOpen = 0;
+configureAndOpenHTTPDIfNeeded();
+
+if ($downloadTest) {
+    system "/usr/bin/curl -o ~/Desktop/iexploder$downloadTest.html \"http://127.0.0.1:$httpdPort/iexploder.cgi?lookup=1&test=$downloadTest\"";
+    print "Saved the test as iexploder$downloadTest.html on the desktop\n";
+} else {
+    runSafariWithIExploder();
+    print "Last generated tests:\n";
+    system "grep 'iexploder.cgi' $iExploderTestDirectory/access_log.txt | tail -n -5 | awk -F'[ =&\\?]' '{if (\$8 == \"lookup\") print \$11; else print \$9}'";
+}
+
+rmtree $iExploderTestDirectory;
+$isHttpdOpen = !closeHTTPD();
+
+sub runSafariWithIExploder()
+{
+    my $redirectTo;
+    if (@ARGV) {
+        $redirectTo = "http://127.0.0.1:$httpdPort/iexploder.cgi?lookup=1&test=$ARGV[0]";
+    } else {
+        $redirectTo = "http://127.0.0.1:$httpdPort/index.html";
+    }
+
+    open REDIRECT_HTML, ">", "$iExploderTestDirectory/redirect.html" or die;
+    print REDIRECT_HTML "<html>\n";
+    print REDIRECT_HTML "    <head>\n";
+    print REDIRECT_HTML "        <meta http-equiv=\"refresh\" content=\"1;URL=$redirectTo\" />\n";
+    print REDIRECT_HTML "        <script type=\"text/javascript\">\n";
+    print REDIRECT_HTML "            document.location = \"$redirectTo\";\n";
+    print REDIRECT_HTML "        </script>\n";
+    print REDIRECT_HTML "    </head>\n";
+    print REDIRECT_HTML "    <body>\n";
+    print REDIRECT_HTML "    </body>\n";
+    print REDIRECT_HTML "</html>\n";
+    close REDIRECT_HTML;
+    
+    if (!isAppleWebKit()) {
+        system "Tools/Scripts/run-launcher", "$iExploderTestDirectory/redirect.html";
+    } else {
+        local %ENV;
+        $ENV{DYLD_INSERT_LIBRARIES} = "/usr/lib/libgmalloc.dylib" if $guardMalloc;
+        system "Tools/Scripts/run-safari", "-NSOpen", "$iExploderTestDirectory/redirect.html";
+    }
+}
+
+sub configureAndOpenHTTPDIfNeeded()
+{
+    return if $isHttpdOpen;
+    mkdir $iExploderTestDirectory;
+    my $webkitDirectory = getcwd();
+    my $testDirectory = $webkitDirectory . "/LayoutTests";
+    my $iExploderDirectory = $webkitDirectory . "/Tools/iExploder/iExploder-1.3.2";
+
+    my $httpdConfig = getHTTPDConfigPathForTestDirectory($testDirectory);
+
+    my $documentRoot = "$iExploderDirectory/htdocs";
+    my $typesConfig = "$testDirectory/http/conf/mime.types";
+    my $sslCertificate = "$testDirectory/http/conf/webkit-httpd.pem";
+    my $listen = "127.0.0.1:$httpdPort";
+
+
+    my @args = (
+        "-f", "$httpdConfig",
+        "-C", "DocumentRoot \"$documentRoot\"",
+        "-C", "Listen $listen",
+        "-c", "TypesConfig \"$typesConfig\"",
+        "-c", "CustomLog \"$iExploderTestDirectory/access_log.txt\" common",
+        "-c", "ErrorLog \"$iExploderTestDirectory/error_log.txt\"",
+        "-c", "SSLCertificateFile \"$sslCertificate\"",
+        # Apache wouldn't run CGIs with permissions==700 otherwise
+        "-c", "User \"#$<\""
+    );
+
+    $isHttpdOpen = openHTTPD(@args);
+}
diff --git a/Tools/Scripts/run-inspector-perf-tests.py b/Tools/Scripts/run-inspector-perf-tests.py
new file mode 100755
index 0000000..92082f7
--- /dev/null
+++ b/Tools/Scripts/run-inspector-perf-tests.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+# Copyright (C) 2011 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.
+
+"""Run Inspector's perf tests in perf mode."""
+
+import logging
+import sys
+
+from webkitpy.performance_tests.perftestsrunner import PerfTestsRunner
+
+_log = logging.getLogger(__name__)
+
+if '__main__' == __name__:
+    logging.basicConfig(level=logging.INFO, format="%(message)s")
+    sys.exit(PerfTestsRunner(args=['inspector']).run())
diff --git a/Tools/Scripts/run-javascriptcore-tests b/Tools/Scripts/run-javascriptcore-tests
new file mode 100755
index 0000000..9cb4c48
--- /dev/null
+++ b/Tools/Scripts/run-javascriptcore-tests
@@ -0,0 +1,209 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005 Apple Computer, Inc.  All rights reserved.
+# Copyright (C) 2007 Eric Seidel <eric@webkit.org>
+#
+# 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.
+
+# Script to run the WebKit Open Source Project JavaScriptCore tests (adapted from Mozilla).
+
+use strict;
+use FindBin;
+use Getopt::Long qw(:config pass_through);
+use lib $FindBin::Bin;
+use webkitdirs;
+use POSIX;
+
+# determine configuration
+setConfiguration();
+my $configuration = configuration();
+
+my @testsToSkip = (
+    # Various ecma/Date tests sometimes fail on Windows (but not Mac) https://bugs.webkit.org/show_bug.cgi?id=25160
+    "ecma/Date/15.9.2.1.js",
+    "ecma/Date/15.9.2.2-1.js",
+    "ecma/Date/15.9.2.2-2.js",
+    "ecma/Date/15.9.2.2-3.js",
+    "ecma/Date/15.9.2.2-4.js",
+    "ecma/Date/15.9.2.2-5.js",
+    "ecma/Date/15.9.2.2-6.js",
+    # ecma_3/Date/15.9.5.7.js fails on Mac (but not Windows) https://bugs.webkit.org/show_bug.cgi?id=25161
+    "ecma_3/Date/15.9.5.6.js",
+    "ecma_3/Date/15.9.5.7.js",
+    # These three fail on Linux in certain time zones, at certain times
+    # of the year (!): https://bugs.webkit.org/show_bug.cgi?id=71371
+    "ecma/Date/15.9.5.14.js",
+    "ecma/Date/15.9.5.31-1.js",
+    "ecma/Date/15.9.5.34-1.js",
+);
+
+my $jsDriverArgs = "-L " . join(" ", @testsToSkip);
+# These variables are intentionally left undefined.
+my $root;
+my $showHelp;
+
+my $buildJSC = 1;
+
+my $programName = basename($0);
+my $buildJSCDefault = $buildJSC ? "will check" : "will not check";
+my $usage = <<EOF;
+Usage: $programName [options] [options to pass to build system]
+  --help                        Show this help message
+  --jsDriver-args=              A string of arguments to pass to jsDriver.pl
+  --root=                       Path to pre-built root containing jsc
+  --[no-]build                  Check (or don't check) to see if the jsc build is up-to-date (default: $buildJSCDefault)
+EOF
+
+GetOptions(
+    'j|jsDriver-args=s' => \$jsDriverArgs,
+    'root=s' => \$root,
+    'build!' => \$buildJSC,
+    'help' => \$showHelp
+);
+
+# Assume any arguments left over from GetOptions are assumed to be build arguments
+my @buildArgs = @ARGV;
+
+# Arguments passed to --jsDriver-args (if any) are passed to jsDriver.pl
+my @jsArgs = split(" ", $jsDriverArgs);
+
+if ($showHelp) {
+   print STDERR $usage;
+   exit 1;
+}
+
+setConfigurationProductDir(Cwd::abs_path($root)) if (defined($root));
+
+if (!defined($root) && $buildJSC) {
+    chdirWebKit();
+
+    push(@buildArgs, argumentsForConfiguration());
+    
+    print "Running: build-jsc " . join(" ", @buildArgs) . "\n";
+    my $buildResult = system "perl", "Tools/Scripts/build-jsc", @buildArgs;
+    if ($buildResult) {
+        print STDERR "Compiling jsc failed!\n";
+        exit exitStatus($buildResult);
+    }
+}
+
+
+my $productDir = jscProductDir();
+$ENV{DYLD_FRAMEWORK_PATH} = $productDir;
+setPathForRunningWebKitApp(\%ENV) if isCygwin();
+
+sub testapiPath($)
+{
+    my ($productDir) = @_;
+    my $jscName = "testapi";
+    $jscName .= "_debug" if configurationForVisualStudio() eq "Debug_All";
+    return "$productDir/$jscName";
+}
+
+#run api tests
+if (isAppleMacWebKit() || isAppleWinWebKit()) {
+    chdirWebKit();
+    chdir($productDir) or die "Failed to switch directory to '$productDir'\n";
+    my $path = testapiPath($productDir);
+    # Use an "indirect object" so that system() won't get confused if the path
+    # contains spaces (see perldoc -f exec).
+    my $testapiResult = system { $path } $path;
+    exit exitStatus($testapiResult)  if $testapiResult;
+}
+
+# Find JavaScriptCore directory
+chdirWebKit();
+chdir("Source/JavaScriptCore");
+chdir "tests/mozilla" or die "Failed to switch directory to 'tests/mozilla'\n";
+printf "Running: jsDriver.pl -e squirrelfish -s %s -f actual.html %s\n", jscPath($productDir), join(" ", @jsArgs);
+my @jsDriverCmd = ("perl", "jsDriver.pl", "-e", "squirrelfish", "-s", jscPath($productDir), "-f", "actual.html", @jsArgs);
+if (isGtk() || isEfl()) {
+    my $jhbuildPrefix = sourceDir() . "/Tools/";
+    $jhbuildPrefix .= isEfl() ? "efl" : "";
+    $jhbuildPrefix .= isGtk() ? "gtk" : "";
+    $jhbuildPrefix .= "/run-with-jhbuild";
+    unshift(@jsDriverCmd, $jhbuildPrefix);
+}
+my $result = system(@jsDriverCmd);
+exit exitStatus($result)  if $result;
+
+my %failures;
+
+open EXPECTED, "expected.html" or die "Failed to open 'expected.html'\n";
+while (<EXPECTED>) {
+    last if /failures reported\.$/;
+}
+while (<EXPECTED>) {
+    chomp;
+    $failures{$_} = 1;
+}
+close EXPECTED;
+
+my %newFailures;
+
+open ACTUAL, "actual.html" or die "Failed to open 'actual.html'";
+while (<ACTUAL>) {
+    last if /failures reported\.$/;
+}
+while (<ACTUAL>) {
+    chomp;
+    if ($failures{$_}) {
+        delete $failures{$_};
+    } else {
+        $newFailures{$_} = 1;
+    }
+}
+close ACTUAL;
+
+my $numNewFailures = keys %newFailures;
+if ($numNewFailures) {
+    print "\n** Danger, Will Robinson! Danger! The following failures have been introduced:\n";
+    foreach my $failure (sort keys %newFailures) {
+        print "\t$failure\n";
+    }
+}
+
+my $numOldFailures = keys %failures;
+if ($numOldFailures) {
+    print "\nYou fixed the following test";
+    print "s" if $numOldFailures != 1;
+    print ":\n";
+    foreach my $failure (sort keys %failures) {
+        print "\t$failure\n";
+    }
+}
+
+print "\n";
+
+print "$numNewFailures regression";
+print "s" if $numNewFailures != 1;
+print " found.\n";
+
+print "$numOldFailures test";
+print "s" if $numOldFailures != 1;
+print " fixed.\n";
+
+print "OK.\n" if $numNewFailures == 0;
+exit(1)  if $numNewFailures;
diff --git a/Tools/Scripts/run-jsc b/Tools/Scripts/run-jsc
new file mode 100755
index 0000000..e5341c1
--- /dev/null
+++ b/Tools/Scripts/run-jsc
@@ -0,0 +1,59 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2006 Apple Computer, Inc.  All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+#     its contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# This script runs a list of scripts through jsc a specified number of times.
+
+use strict;
+use warnings;
+use File::Spec;
+use FindBin;
+use lib $FindBin::Bin;
+use Getopt::Long;
+use webkitdirs;
+
+my $usage = "Usage: run-jsc [--count run_count] [--verbose] shell_file [file2...]";
+
+my $count = 1;
+my $verbose = 0;
+GetOptions("count|c=i" => \$count,
+           "verbose|v" => \$verbose);
+die "$usage\n" if (@ARGV < 1);
+
+my $jsc = jscProductDir() . "/jsc @ARGV";
+$jsc .= " 2> " . File::Spec->devnull() unless $verbose;
+
+my $dyld = jscProductDir();
+
+$ENV{"DYLD_FRAMEWORK_PATH"} = $dyld;
+print STDERR "Running $count time(s): DYLD_FRAMEWORK_PATH=$dyld $jsc\n";
+while ($count--) {
+    if (system("$jsc") != 0) {
+        last;
+    }
+}
+
diff --git a/Tools/Scripts/run-launcher b/Tools/Scripts/run-launcher
new file mode 100755
index 0000000..eca07f6
--- /dev/null
+++ b/Tools/Scripts/run-launcher
@@ -0,0 +1,97 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2007 Apple Computer, Inc.  All rights reserved.
+# Copyright (C) 2007 Staikos Computing Services, Inc.  <info@staikos.net>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer. 
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution. 
+# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+#     its contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission. 
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Simplified "run" script for WebKit Open Source Project.
+
+use strict;
+use File::Spec::Functions qw/catdir/;
+use File::Temp qw/tempfile/;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+setConfiguration();
+my $productDir = productDir();
+my $launcherPath = productDir();
+
+# Check to see that all the frameworks are built.
+checkFrameworks();
+
+# Set paths according to the build system used
+if (isQt()) {
+    my $libDir = catdir(productDir(), 'lib');
+    if (isWK2()) {
+        $launcherPath = catdir($launcherPath, "bin", "MiniBrowser");
+    } else {
+        $launcherPath = catdir($launcherPath, "bin", "QtTestBrowser");
+    }
+
+    $ENV{QTWEBKIT_PLUGIN_PATH} = catdir($libDir, 'plugins');
+
+    print "Starting webkit launcher, running against the built WebKit in $libDir...\n";
+    if (isDarwin()) {
+        $ENV{DYLD_LIBRARY_PATH} = $ENV{DYLD_LIBRARY_PATH} ? "$libDir:$ENV{DYLD_LIBRARY_PATH}" : $libDir;
+        $ENV{DYLD_FRAMEWORK_PATH} = $ENV{DYLD_FRAMEWORK_PATH} ? "$libDir:$ENV{DYLD_FRAMEWORK_PATH}" : $libDir;
+    } else {
+        $ENV{LD_LIBRARY_PATH} = $ENV{LD_LIBRARY_PATH} ? "$libDir:$ENV{LD_LIBRARY_PATH}" : $libDir;
+    }
+} else {
+
+    if (isGtk()) {
+        if (isWK2()) {
+            unshift(@ARGV, catdir($launcherPath, "Programs", "MiniBrowser"));
+        } else {
+            unshift(@ARGV, catdir($launcherPath, "Programs", "GtkLauncher"));
+        }
+        $launcherPath = catdir(sourceDir(), "Tools", "gtk", "run-with-jhbuild");
+    }
+    
+    if (isEfl()) {
+        if (isWK2()) {
+            unshift(@ARGV, catdir($launcherPath, "bin", "MiniBrowser"));
+        } else {
+            unshift(@ARGV, catdir($launcherPath, "bin", "EWebLauncher"));
+        }
+        $launcherPath = catdir(sourceDir(), "Tools", "efl", "run-with-jhbuild");
+    }
+    
+    if (isWx()) {
+        if (isDarwin()) {
+            $launcherPath = catdir($launcherPath, 'wxBrowser.app', 'Contents', 'MacOS', 'wxBrowser');
+        } else {
+            $ENV{LD_LIBRARY_PATH} = $ENV{LD_LIBRARY_PATH} ? "$productDir:$ENV{LD_LIBRARY_PATH}" : $productDir;
+            $launcherPath = catdir($launcherPath, 'wxBrowser');
+        }
+    }
+
+    print "Starting webkit launcher.\n";
+}
+
+exec $launcherPath, @ARGV or die;
+
diff --git a/Tools/Scripts/run-leaks b/Tools/Scripts/run-leaks
new file mode 100755
index 0000000..4642637
--- /dev/null
+++ b/Tools/Scripts/run-leaks
@@ -0,0 +1,218 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2007 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.
+
+# Script to run the Mac OS X leaks tool with more expressive '-exclude' lists.
+
+use strict;
+use warnings;
+
+use File::Basename;
+use Getopt::Long;
+
+sub runLeaks($);
+sub parseLeaksOutput(\@);
+sub removeMatchingRecords(\@$\@);
+sub reportError($);
+
+sub main()
+{
+    # Read options.
+    my $usage =
+        "Usage: " . basename($0) . " [options] pid | executable name\n" .
+        "  --exclude-callstack regexp   Exclude leaks whose call stacks match the regular expression 'regexp'.\n" .
+        "  --exclude-type regexp        Exclude leaks whose data types match the regular expression 'regexp'.\n" .
+        "  --help                       Show this help message.\n";
+
+    my @callStacksToExclude = ();
+    my @typesToExclude = ();
+    my $help = 0;
+
+    my $getOptionsResult = GetOptions(
+        'exclude-callstack:s' => \@callStacksToExclude,
+        'exclude-type:s' => \@typesToExclude,
+        'help' => \$help
+    );
+    my $pidOrExecutableName = $ARGV[0];
+
+    if (!$getOptionsResult || $help) {
+        print STDERR $usage;
+        return 1;
+    }
+
+    if (!$pidOrExecutableName) {
+        reportError("Missing argument: pid | executable.");
+        print STDERR $usage;
+        return 1;
+    }
+
+    # Run leaks tool.
+    my $leaksOutput = runLeaks($pidOrExecutableName);
+    if (!$leaksOutput) {
+        return 1;
+    }
+
+    my $leakList = parseLeaksOutput(@$leaksOutput);
+    if (!$leakList) {
+        return 1;
+    }
+
+    # Filter output.
+    my $leakCount = @$leakList;
+    removeMatchingRecords(@$leakList, "callStack", @callStacksToExclude);
+    removeMatchingRecords(@$leakList, "type", @typesToExclude);
+    my $excludeCount = $leakCount - @$leakList;
+
+    # Dump results.
+    print $leaksOutput->[0];
+    print $leaksOutput->[1];
+    foreach my $leak (@$leakList) {
+        print $leak->{"leaksOutput"};
+    }
+
+    if ($excludeCount) {
+        print "$excludeCount leaks excluded (not printed)\n";
+    }
+
+    return 0;
+}
+
+exit(main());
+
+# Returns the output of the leaks tool in list form.
+sub runLeaks($)
+{
+    my ($pidOrExecutableName) = @_;
+    
+    my @leaksOutput = `leaks $pidOrExecutableName`;
+    if (!@leaksOutput) {
+        reportError("Error running leaks tool.");
+        return;
+    }
+    
+    return \@leaksOutput;
+}
+
+# Returns a list of hash references with the keys { address, size, type, callStack, leaksOutput }
+sub parseLeaksOutput(\@)
+{
+    my ($leaksOutput) = @_;
+
+    # Format:
+    #   Process 00000: 1234 nodes malloced for 1234 KB
+    #   Process 00000: XX leaks for XXX total leaked bytes.    
+    #   Leak: 0x00000000 size=1234 [instance of 'blah']
+    #       0x00000000 0x00000000 0x00000000 0x00000000 a..d.e.e
+    #       ...
+    #       Call stack: leak_caller() | leak() | malloc
+    #
+    #   We treat every line except for  Process 00000: and Leak: as optional
+
+    # Skip header section until the first two "Process " lines.
+    # FIXME: In the future we may wish to propagate the header section through to our output.
+    until ($leaksOutput->[0] =~ /^Process /) {
+        shift @$leaksOutput;
+    }
+
+    my ($leakCount) = ($leaksOutput->[1] =~ /[[:blank:]]+([0-9]+)[[:blank:]]+leaks?/);
+    if (!defined($leakCount)) {
+        reportError("Could not parse leak count reported by leaks tool.");
+        return;
+    }
+
+    my @leakList = ();
+    for my $line (@$leaksOutput) {
+        next if $line =~ /^Process/;
+        next if $line =~ /^node buffer added/;
+        
+        if ($line =~ /^Leak: /) {
+            my ($address) = ($line =~ /Leak: ([[:xdigit:]x]+)/);
+            if (!defined($address)) {
+                reportError("Could not parse Leak address.");
+                return;
+            }
+
+            my ($size) = ($line =~ /size=([[:digit:]]+)/);
+            if (!defined($size)) {
+                reportError("Could not parse Leak size.");
+                return;
+            }
+
+            my ($type) = ($line =~ /'([^']+)'/); #'
+            if (!defined($type)) {
+                $type = ""; # The leaks tool sometimes omits the type.
+            }
+
+            my %leak = (
+                "address" => $address,
+                "size" => $size,
+                "type" => $type,
+                "callStack" => "", # The leaks tool sometimes omits the call stack.
+                "leaksOutput" => $line
+            );
+            push(@leakList, \%leak);
+        } else {
+            $leakList[$#leakList]->{"leaksOutput"} .= $line;
+            if ($line =~ /Call stack:/) {
+                $leakList[$#leakList]->{"callStack"} = $line;
+            }
+        }
+    }
+    
+    if (@leakList != $leakCount) {
+        my $parsedLeakCount = @leakList;
+        reportError("Parsed leak count($parsedLeakCount) does not match leak count reported by leaks tool($leakCount).");
+        return;
+    }
+
+    return \@leakList;
+}
+
+sub removeMatchingRecords(\@$\@)
+{
+    my ($recordList, $key, $regexpList) = @_;
+    
+    RECORD: for (my $i = 0; $i < @$recordList;) {
+        my $record = $recordList->[$i];
+
+        foreach my $regexp (@$regexpList) {
+            if ($record->{$key} =~ $regexp) {
+                splice(@$recordList, $i, 1);
+                next RECORD;
+            }
+        }
+        
+        $i++;
+    }
+}
+
+sub reportError($)
+{
+    my ($errorMessage) = @_;
+    
+    print STDERR basename($0) . ": $errorMessage\n";
+}
diff --git a/Tools/Scripts/run-mangleme-tests b/Tools/Scripts/run-mangleme-tests
new file mode 100755
index 0000000..10196ef
--- /dev/null
+++ b/Tools/Scripts/run-mangleme-tests
@@ -0,0 +1,176 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2006 Alexey Proskuryakov (ap@nypop.com)
+#
+# 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.
+
+# A script to semi-automatically run mangleme tests.
+
+use strict;
+use warnings;
+
+use Cwd;
+use File::Spec;
+use FindBin;
+use Getopt::Long;
+use IPC::Open2;
+
+use lib $FindBin::Bin;
+use webkitdirs;
+
+sub openHTTPDIfNeeded();
+sub closeHTTPD();
+sub runSafariWithMangleme();
+
+# Argument handling
+my $guardMalloc = '';
+my $httpdPort = 8000;
+my $downloadTest;
+
+GetOptions(
+    'guard-malloc|g' => \$guardMalloc,
+    'get=s' => \$downloadTest,
+    'port=i' => \$httpdPort
+);
+
+
+setConfiguration();
+my $productDir = productDir();
+
+chdirWebKit();
+
+checkFrameworks();
+
+mkdir "WebKitBuild/mangleme";
+(system "/usr/bin/make", "-C", "Tools/mangleme") == 0 or die;
+
+my $httpdOpen = 0;
+openHTTPDIfNeeded();
+
+if ($downloadTest) {
+    system "/usr/bin/curl -o ~/Desktop/mangleme$downloadTest.html http://127.0.0.1:$httpdPort/remangle.cgi?$downloadTest";
+    print "Saved the test as mangleme$downloadTest.html on the desktop\n";
+} else {
+    runSafariWithMangleme();
+    print "Last generated tests:\n";
+    system "grep 'Mangle attempt' /tmp/WebKit/error_log.txt | tail -n -5 | awk ' {print \$4}'";
+}
+
+closeHTTPD();
+
+
+sub runSafariWithMangleme()
+{
+    my $redirectTo;
+    if (@ARGV) {
+        $redirectTo = "http://127.0.0.1:$httpdPort/remangle.cgi?$ARGV[0]";
+    } else {
+        $redirectTo = "http://127.0.0.1:$httpdPort/mangle.cgi";
+    }
+
+    open REDIRECT_HTML, ">", "/tmp/WebKit/redirect.html" or die;
+    print REDIRECT_HTML "<html>\n";
+    print REDIRECT_HTML "    <head>\n";
+    print REDIRECT_HTML "        <meta http-equiv=\"refresh\" content=\"1;URL=$redirectTo\" />\n";
+    print REDIRECT_HTML "        <script type=\"text/javascript\">\n";
+    print REDIRECT_HTML "            document.location = \"$redirectTo\";\n";
+    print REDIRECT_HTML "        </script>\n";
+    print REDIRECT_HTML "    </head>\n";
+    print REDIRECT_HTML "    <body>\n";
+    print REDIRECT_HTML "    </body>\n";
+    print REDIRECT_HTML "</html>\n";
+    close REDIRECT_HTML;
+    
+    local %ENV;
+    $ENV{DYLD_INSERT_LIBRARIES} = "/usr/lib/libgmalloc.dylib" if $guardMalloc;
+    system "Tools/Scripts/run-safari", "-NSOpen", "/tmp/WebKit/redirect.html";
+}
+
+sub openHTTPDIfNeeded()
+{
+    return if $httpdOpen;
+
+    mkdir "/tmp/WebKit";
+    
+    if (-f "/tmp/WebKit/httpd.pid") {
+        my $oldPid = `cat /tmp/WebKit/httpd.pid`;
+        chomp $oldPid;
+        if (0 != kill 0, $oldPid) {
+            print "\nhttpd is already running: pid $oldPid, killing...\n";
+            kill 15, $oldPid;
+            
+            my $retryCount = 20;
+            while ((0 != kill 0, $oldPid) && $retryCount) {
+                sleep 1;
+                --$retryCount;
+            }
+            
+            die "Timed out waiting for httpd to quit" unless $retryCount;
+        }
+    }
+    
+    my $testDirectory = getcwd() . "/LayoutTests";
+    my $manglemeDirectory = getcwd() . "/WebKitBuild/mangleme";
+    my $httpdPath = "/usr/sbin/httpd";
+    my $httpdConfig = "$testDirectory/http/conf/httpd.conf";
+    $httpdConfig = "$testDirectory/http/conf/apache2-httpd.conf" if `$httpdPath -v` =~ m|Apache/2|;
+    my $documentRoot = "$manglemeDirectory";
+    my $typesConfig = "$testDirectory/http/conf/mime.types";
+    my $sslCertificate = "$testDirectory/http/conf/webkit-httpd.pem";
+    my $listen = "127.0.0.1:$httpdPort";
+
+    open2(\*HTTPDIN, \*HTTPDOUT, $httpdPath, 
+        "-f", "$httpdConfig",
+        "-C", "DocumentRoot \"$documentRoot\"",
+        "-C", "Listen $listen",
+        "-c", "TypesConfig \"$typesConfig\"",
+        "-c", "CustomLog \"/tmp/WebKit/access_log.txt\" common",
+        "-c", "ErrorLog \"/tmp/WebKit/error_log.txt\"",
+        "-c", "SSLCertificateFile \"$sslCertificate\"",
+        # Apache wouldn't run CGIs with permissions==700 otherwise
+        "-c", "User \"#$<\"");
+
+    my $retryCount = 20;
+    while (system("/usr/bin/curl -q --silent --stderr - --output " . File::Spec->devnull() . " $listen") && $retryCount) {
+        sleep 1;
+        --$retryCount;
+    }
+    
+    die "Timed out waiting for httpd to start" unless $retryCount;
+    
+    $httpdOpen = 1;
+}
+
+sub closeHTTPD()
+{
+    return if !$httpdOpen;
+
+    close HTTPDIN;
+    close HTTPDOUT;
+
+    kill 15, `cat /tmp/WebKit/httpd.pid` if -f "/tmp/WebKit/httpd.pid";
+
+    $httpdOpen = 0;
+}
diff --git a/Tools/Scripts/run-minibrowser b/Tools/Scripts/run-minibrowser
new file mode 100755
index 0000000..c841651
--- /dev/null
+++ b/Tools/Scripts/run-minibrowser
@@ -0,0 +1,40 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005, 2007 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 MiniBrowser.
+
+use strict;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+printHelpAndExitForRunAndDebugWebKitAppIfNeeded();
+
+setConfiguration();
+
+exit exitStatus(runMiniBrowser());
diff --git a/Tools/Scripts/run-pageloadtest b/Tools/Scripts/run-pageloadtest
new file mode 100755
index 0000000..3542aa9
--- /dev/null
+++ b/Tools/Scripts/run-pageloadtest
@@ -0,0 +1,92 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2006 Eric Seidel (eric@webkit.org)
+#
+# 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.
+
+# Script to run the WebKit Open Source Project page load tests (PLTs).
+
+# Run all the tests passed in on the command line.
+
+use strict;
+use warnings;
+
+use File::Basename;
+use File::Spec;
+use FindBin;
+use Getopt::Long;
+
+use lib $FindBin::Bin;
+use webkitdirs;
+
+# Argument handling
+my $testName = 'svg';
+my $showHelp = 0;
+
+my $usage =
+    "Usage: " . basename($0) . "[options] testName\n" .
+    "  --help                  Show this help message\n";
+
+my $getOptionsResult = GetOptions('help' => \$showHelp);
+
+if (!$getOptionsResult || $showHelp) {
+    print STDERR $usage;
+    exit 1;
+}
+
+$testName = shift @ARGV if (@ARGV);
+
+my $safariExecutablePath = safariPath();
+my $safariResourcePath = File::Spec->catdir(dirname(dirname($safariExecutablePath)), "Resources");
+
+# Check to see that all the frameworks are built.
+checkFrameworks();
+
+chdirWebKit();
+
+if ($testName eq 'svg') {
+    my $suiteFile = "PerformanceTests/PageLoad/$testName/$testName.pltsuite";
+    my $webkitPath = sourceDir();
+    `cat "$suiteFile" | perl -pe 's|WEBKIT_PATH|$webkitPath|' > $safariResourcePath/$testName.pltsuite`
+}
+
+die "Please copy ${testName}.pltsuite to ${safariResourcePath}/${testName}.pltsuite"
+    if (! -f "${safariResourcePath}/${testName}.pltsuite");
+
+setConfiguration();
+
+my $productDir = productDir();
+
+# Set up DYLD_FRAMEWORK_PATH to point to the product directory.
+print "Starting Safari with DYLD_FRAMEWORK_PATH set to point to built WebKit in $productDir.\n";
+$ENV{DYLD_FRAMEWORK_PATH} = $productDir;
+$ENV{WEBKIT_UNSET_DYLD_FRAMEWORK_PATH} = "YES";
+
+my @testCommands = ('activate');
+# Autovicki would clear history, we skip that here as this is likely an active user account
+@testCommands = (@testCommands, ("run $testName", 'emptyCache', 'wait 30'));
+@testCommands = (@testCommands, (("run $testName", 'wait 10') x 3));
+my $testCommandsString = join('; ', @testCommands);
+exec $safariExecutablePath, '--test-commands', $testCommandsString or die;
diff --git a/Tools/Scripts/run-perf-tests b/Tools/Scripts/run-perf-tests
new file mode 100755
index 0000000..1bf8ec8
--- /dev/null
+++ b/Tools/Scripts/run-perf-tests
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# Copyright (C) 2012 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.
+
+"""Run performance tests."""
+
+import logging
+import sys
+
+from webkitpy.performance_tests.perftestsrunner import PerfTestsRunner
+
+if '__main__' == __name__:
+    logging.basicConfig(level=logging.INFO, format="%(message)s")
+
+    sys.exit(PerfTestsRunner().run())
diff --git a/Tools/Scripts/run-qtwebkit-tests b/Tools/Scripts/run-qtwebkit-tests
new file mode 100755
index 0000000..ded87c5
--- /dev/null
+++ b/Tools/Scripts/run-qtwebkit-tests
@@ -0,0 +1,415 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+#Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies)
+
+#This library is free software; you can redistribute it and/or
+#modify it under the terms of the GNU Library General Public
+#License as published by the Free Software Foundation; either
+#version 2 of the License, or (at your option) any later version.
+
+#This library is distributed in the hope that it will be useful,
+#but WITHOUT ANY WARRANTY; without even the implied warranty of
+#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#Library General Public License for more details.
+
+#You should have received a copy of the GNU Library General Public License
+#along with this library; see the file COPYING.LIB.  If not, write to
+#the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+#Boston, MA 02110-1301, USA.
+
+from __future__ import with_statement
+
+import sys
+import os
+import re
+import logging
+from subprocess import Popen, PIPE, STDOUT
+from optparse import OptionParser
+
+
+class Log(object):
+    def __init__(self, name):
+        self._log = logging.getLogger(name)
+        self.debug = self._log.debug
+        self.warn = self._log.warn
+        self.error = self._log.error
+        self.exception = self._log.exception
+        self.info = self._log.info
+
+
+class Options(Log):
+    """ Option manager. It parses and checks script's parameters, sets an internal variable. """
+
+    def __init__(self, args):
+        Log.__init__(self, "Options")
+        log = self._log
+        opt = OptionParser("%prog [options] PathToSearch.\nTry -h or --help.")
+        opt.add_option("-j", "--parallel-level", action="store", type="int",
+              dest="parallel_level", default=None,
+              help="Number of parallel processes executing the Qt's tests. Default: cpu count.")
+        opt.add_option("-v", "--verbose", action="store", type="int",
+              dest="verbose", default=2,
+              help="Verbose level (0 - quiet, 1 - errors only, 2 - infos and warnings, 3 - debug information). Default: %default.")
+        opt.add_option("", "--tests-options", action="store", type="string",
+              dest="tests_options", default="",
+              help="Parameters passed to Qt's tests (for example '-eventdelay 123').")
+        opt.add_option("-o", "--output-file", action="store", type="string",
+              dest="output_file", default="/tmp/qtwebkittests.html",
+              help="File where results will be stored. The file will be overwritten. Default: %default.")
+        opt.add_option("-b", "--browser", action="store", dest="browser",
+              default="xdg-open",
+              help="Browser in which results will be opened. Default %default.")
+        opt.add_option("", "--do-not-open-results", action="store_false",
+              dest="open_results", default=True,
+              help="The results shouldn't pop-up in a browser automatically")
+        opt.add_option("-d", "--developer-mode", action="store_true",
+              dest="developer", default=False,
+              help="Special mode for debugging. In general it simulates human behavior, running all autotests. In the mode everything is executed synchronously, no html output will be generated, no changes or transformation will be applied to stderr or stdout. In this mode options; parallel-level, output-file, browser and do-not-open-results will be ignored.")
+        opt.add_option("-t", "--timeout", action="store", type="int",
+              dest="timeout", default=0,
+              help="Timeout in seconds for each testsuite. Zero value means that there is not timeout. Default: %default.")
+
+        self._o, self._a = opt.parse_args(args)
+        verbose = self._o.verbose
+        if verbose == 0:
+            logging.basicConfig(level=logging.CRITICAL,)
+        elif verbose == 1:
+            logging.basicConfig(level=logging.ERROR,)
+        elif verbose == 2:
+            logging.basicConfig(level=logging.INFO,)
+        elif verbose == 3:
+            logging.basicConfig(level=logging.DEBUG,)
+        else:
+            logging.basicConfig(level=logging.INFO,)
+            log.warn("Bad verbose level, switching to default.")
+        try:
+            if not os.path.exists(self._a[0]):
+                raise Exception("Given path doesn't exist.")
+            if len(self._a) > 1:
+                raise IndexError("Only one directory could be provided.")
+            self._o.path = self._a[0]
+        except IndexError:
+            log.error("Bad usage. Please try -h or --help.")
+            sys.exit(1)
+        except Exception:
+            log.error("Path '%s' doesn't exist", self._a[0])
+            sys.exit(2)
+        if self._o.developer:
+            if not self._o.parallel_level is None:
+                log.warn("Developer mode sets parallel-level option to one.")
+            self._o.parallel_level = 1
+            self._o.open_results = False
+
+    def __getattr__(self, attr):
+        """ Maps all options properties into this object (remove one level of indirection). """
+        return getattr(self._o, attr)
+
+
+def run_test(args):
+    """ Runs one given test.
+    args should contain a tuple with 3 elements;
+      TestSuiteResult containing full file name of an autotest executable.
+      str with options that should be passed to the autotest executable
+      bool if true then the stdout will be buffered and separated from the stderr, if it is false
+        then the stdout and the stderr will be merged together and left unbuffered (the TestSuiteResult output will be None).
+      int time after which the autotest executable would be killed
+    """
+    log = logging.getLogger("Exec")
+    test_suite, options, buffered, timeout = args
+    timer = None
+    try:
+        log.info("Running... %s", test_suite.test_file_name())
+        if buffered:
+            tst = Popen([test_suite.test_file_name()] + options.split(), stdout=PIPE, stderr=None)
+        else:
+            tst = Popen([test_suite.test_file_name()] + options.split(), stdout=None, stderr=STDOUT)
+        if timeout:
+            from threading import Timer
+            log.debug("Setting timeout timer %i sec on %s (process %s)", timeout, test_suite.test_file_name(), tst.pid)
+            def process_killer():
+                try:
+                    try:
+                        tst.terminate()
+                    except AttributeError:
+                        # Workaround for python version < 2.6 it can be removed as soon as we drop support for python2.5
+                        try:
+                            import ctypes
+                            PROCESS_TERMINATE = 1
+                            handle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, False, tst.pid)
+                            ctypes.windll.kernel32.TerminateProcess(handle, -1)
+                            ctypes.windll.kernel32.CloseHandle(handle)
+                        except AttributeError:
+                            # windll is not accessible so we are on *nix like system
+                            import signal
+                            os.kill(tst.pid, signal.SIGTERM)
+                    log.error("Timeout, process '%s' (%i) was terminated", test_suite.test_file_name(), tst.pid)
+                except OSError, e:
+                    # the process was finished before got killed
+                    pass
+            timer = Timer(timeout, process_killer)
+            timer.start()
+    except OSError, e:
+        log.exception("Can't open an autotest file: '%s'. Skipping the test...", e.filename)
+    else:
+        test_suite.set_output(tst.communicate()[0])  # takes stdout only, in developer mode it would be None.
+    log.info("Finished %s", test_suite.test_file_name())
+    if timeout:
+        timer.cancel()
+    return test_suite
+
+
+class TestSuiteResult(object):
+    """ Keeps information about a test. """
+
+    def __init__(self):
+        self._output = None
+        self._test_file_name = None
+
+    def set_output(self, xml):
+        if xml:
+            self._output = xml.strip()
+
+    def output(self):
+        return self._output
+
+    def set_test_file_name(self, file_name):
+        self._test_file_name = file_name
+
+    def test_file_name(self):
+        return self._test_file_name
+
+
+class Main(Log):
+    """ The main script. All real work is done in run() method. """
+
+    def __init__(self, options):
+        Log.__init__(self, "Main")
+        self._options = options
+        if options.parallel_level > 1 or options.parallel_level is None:
+            try:
+                from multiprocessing import Pool
+            except ImportError:
+                self.warn("Import Error: the multiprocessing module couldn't be loaded (may be lack of python-multiprocessing package?). The Qt autotests will be executed one by one.")
+                options.parallel_level = 1
+        if options.parallel_level == 1:
+
+            class Pool(object):
+                """ A hack, created to avoid problems with multiprocessing module, this class is single thread replacement for the multiprocessing.Pool class. """
+                def __init__(self, processes):
+                    pass
+
+                def imap_unordered(self, func, files):
+                    return map(func, files)
+
+                def map(self, func, files):
+                    return map(func, files)
+
+        self._Pool = Pool
+
+    def run(self):
+        """ Find && execute && publish results of all test. "All in one" function. """
+        # This is needed for Qt finding our QML modules. The current code makes our
+        # two existing API tests (WK1 API and WK2 UI process API) work correctly.
+        qml_import_path = self._options.path + "../../../../imports"
+        qml_import_path += ":" + self._options.path + "../../../../../../imports"
+        os.putenv("QML_IMPORT_PATH", qml_import_path)
+        path = os.getenv("PATH")
+        path += ":" + self._options.path + "../../../../../../bin"
+        os.putenv("PATH", path)
+        self.debug("Searching executables...")
+        tests_executables = self.find_tests_paths(self._options.path)
+        self.debug("Found: %s", len(tests_executables))
+        self.debug("Executing tests...")
+        results = self.run_tests(tests_executables)
+        if not self._options.developer:
+            self.debug("Transforming...")
+            transformed_results = self.transform(results)
+            self.debug("Publishing...")
+            self.announce_results(transformed_results)
+
+    def find_tests_paths(self, path):
+        """ Finds all tests executables inside the given path. """
+        executables = []
+        for root, dirs, files in os.walk(path):
+            # Check only for a file that name starts from 'tst_' and that we can execute.
+            filtered_path = filter(lambda w: w.startswith('tst_') and os.access(os.path.join(root, w), os.X_OK), files)
+            filtered_path = map(lambda w: os.path.join(root, w), filtered_path)
+            for file_name in filtered_path:
+                r = TestSuiteResult()
+                r.set_test_file_name(file_name)
+                executables.append(r)
+        return executables
+
+    def run_tests(self, files):
+        """ Executes given files by using a pool of workers. """
+        workers = self._Pool(processes=self._options.parallel_level)
+        # to each file add options.
+        self.debug("Using %s the workers pool, number of workers %i", repr(workers), self._options.parallel_level)
+        package = map(lambda w: [w, self._options.tests_options, not self._options.developer, self._options.timeout], files)
+        self.debug("Generated packages for workers: %s", repr(package))
+        results = workers.map(run_test, package)  # Collects results.
+        return results
+
+    def transform(self, results):
+        """ Transforms list of the results to specialized versions. """
+        stdout = self.convert_to_stdout(results)
+        html = self.convert_to_html(results)
+        return {"stdout": stdout, "html": html}
+
+    def announce_results(self, results):
+        """ Shows the results. """
+        self.announce_results_stdout(results['stdout'])
+        self.announce_results_html(results['html'])
+
+    def announce_results_stdout(self, results):
+        """ Show the results by printing to the stdout."""
+        print(results)
+
+    def announce_results_html(self, results):
+        """ Shows the result by creating a html file and calling a web browser to render it. """
+        with file(self._options.output_file, 'w') as f:
+            f.write(results)
+        if self._options.open_results:
+            Popen(self._options.browser + " " + self._options.output_file, stdout=None, stderr=None, shell=True)
+
+    def check_crash_occurences(self, results):
+        """ Checks if any test crashes and it sums them  """
+        totals = [0,0,0]
+        crash_count = 0
+        txt = []
+        #collecting results into one container with checking crash
+        for result in results:
+            found = None
+            if result.output():
+                txt.append(result.output())
+                found = re.search(r"([0-9]+) passed, ([0-9]+) failed, ([0-9]+) skipped", result.output())
+
+            if found:
+                totals = reduce(lambda x, y: (int(x[0]) + int(y[0]), int(x[1]) + int(y[1]), int(x[2]) + int(y[2])), (totals, found.groups()))
+            else:
+                txt.append('CRASHED: %s' % result.test_file_name())
+                crash_count += 1
+                self.warn("Missing sub-summary: %s" % result.test_file_name())
+
+        txt='\n\n'.join(txt)
+
+        totals = list(totals)
+        totals.append(crash_count)
+        totals = map(str, totals)
+        return txt, totals
+
+    def convert_to_stdout(self, results):
+        """ Converts results, that they could be nicely presented in the stdout. """
+        txt, totals = self.check_crash_occurences(results)
+
+        totals = "%s passed, %s failed, %s skipped, %s crashed" % (totals[0], totals[1], totals[2], totals[3])
+
+        txt += '\n' + '*' * 70
+        txt += "\n**" + ("TOTALS: " + totals).center(66) + '**'
+        txt += '\n' + '*' * 70 + '\n'
+        return txt
+
+    def convert_to_html(self, results):
+        """ Converts results, that they could showed as a html page. """
+        txt, totals = self.check_crash_occurences(results)
+        txt = txt.replace('&', '&amp;').replace('<', "&lt;").replace('>', "&gt;")
+        # Add a color and a style.
+        txt = re.sub(r"([* ]+(Finished)[ a-z_A-Z0-9]+[*]+)",
+            lambda w: r"",
+            txt)
+        txt = re.sub(r"([*]+[ a-z_A-Z0-9]+[*]+)",
+            lambda w: "<case class='good'><br><br><b>" + w.group(0) + r"</b></case>",
+            txt)
+        txt = re.sub(r"(Config: Using QTest library)((.)+)",
+            lambda w: "\n<case class='good'><br><i>" + w.group(0) + r"</i>  ",
+            txt)
+        txt = re.sub(r"\n(PASS)((.)+)",
+            lambda w: "</case>\n<case class='good'><br><status class='pass'>" + w.group(1) + r"</status>" + w.group(2),
+            txt)
+        txt = re.sub(r"\n(FAIL!)((.)+)",
+            lambda w: "</case>\n<case class='bad'><br><status class='fail'>" + w.group(1) + r"</status>" + w.group(2),
+            txt)
+        txt = re.sub(r"\n(XPASS)((.)+)",
+            lambda w: "</case>\n<case class='bad'><br><status class='xpass'>" + w.group(1) + r"</status>" + w.group(2),
+            txt)
+        txt = re.sub(r"\n(XFAIL)((.)+)",
+            lambda w: "</case>\n<case class='good'><br><status class='xfail'>" + w.group(1) + r"</status>" + w.group(2),
+            txt)
+        txt = re.sub(r"\n(SKIP)((.)+)",
+            lambda w: "</case>\n<case class='good'><br><status class='xfail'>" + w.group(1) + r"</status>" + w.group(2),
+            txt)
+        txt = re.sub(r"\n(QWARN)((.)+)",
+            lambda w: "</case>\n<case class='bad'><br><status class='warn'>" + w.group(1) + r"</status>" + w.group(2),
+            txt)
+        txt = re.sub(r"\n(RESULT)((.)+)",
+            lambda w: "</case>\n<case class='good'><br><status class='benchmark'>" + w.group(1) + r"</status>" + w.group(2),
+            txt)
+        txt = re.sub(r"\n(QFATAL|CRASHED)((.)+)",
+            lambda w: "</case>\n<case class='bad'><br><status class='crash'>" + w.group(1) + r"</status>" + w.group(2),
+            txt)
+        txt = re.sub(r"\n(Totals:)([0-9', a-z]*)",
+            lambda w: "</case>\n<case class='good'><br><b>" + w.group(1) + r"</b>" + w.group(2) + "</case>",
+            txt)
+        # Find total count of failed, skipped, passed and crashed tests.
+        totals = "%s passed, %s failed, %s skipped, %s crashed." % (totals[0], totals[1], totals[2], totals[3])
+        # Create a header of the html source.
+        txt = """
+        <html>
+        <head>
+          <script>
+          function init() {
+              // Try to find the right styleSheet (this document could be embedded in an other html doc)
+              for (i = document.styleSheets.length - 1; i >= 0; --i) {
+                  if (document.styleSheets[i].cssRules[0].selectorText == "case.good") {
+                      resultStyleSheet = i;
+                      return;
+                  }
+              }
+              // The styleSheet hasn't been found, but it should be the last one.
+              resultStyleSheet = document.styleSheets.length - 1;
+          }
+
+          function hide() {
+              document.styleSheets[resultStyleSheet].cssRules[0].style.display='none';
+          }
+
+          function show() {
+              document.styleSheets[resultStyleSheet].cssRules[0].style.display='';
+          }
+
+          </script>
+          <style type="text/css">
+            case.good {color:black}
+            case.bad {color:black}
+            status.pass {color:green}
+            status.crash {color:red}
+            status.fail {color:red}
+            status.xpass {color:663300}
+            status.xfail {color:004500}
+            status.benchmark {color:000088}
+            status.warn {color:orange}
+            status.crash {color:red; text-decoration:blink; background-color:black}
+          </style>
+        </head>
+        <body onload="init()">
+        <center>
+          <h1>Qt's autotests results</h1>%(totals)s<br>
+          <hr>
+          <form>
+            <input type="button" value="Show failures only" onclick="hide()"/>
+            &nbsp;
+            <input type="button" value="Show all" onclick="show()"/>
+          </form>
+        </center>
+        <hr>
+        %(results)s
+        </body>
+        </html>""" % {"totals": totals, "results": txt}
+        return txt
+
+
+if __name__ == '__main__':
+    options = Options(sys.argv[1:])
+    main = Main(options)
+    main.run()
diff --git a/Tools/Scripts/run-regexp-tests b/Tools/Scripts/run-regexp-tests
new file mode 100755
index 0000000..dc35ce9
--- /dev/null
+++ b/Tools/Scripts/run-regexp-tests
@@ -0,0 +1,126 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2011 Apple Computer, Inc.  All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer. 
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution. 
+# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+#     its contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission. 
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Script to run the WebKit Open Source Project Regular Expression functional tests.
+
+use strict;
+use FindBin;
+use Getopt::Long qw(:config pass_through);
+use lib $FindBin::Bin;
+use webkitdirs;
+use POSIX;
+
+# determine configuration
+setConfiguration();
+my $configuration = configuration();
+
+my $defaultTestFile = "RegExpTest.data";
+
+# These variables are intentionally left undefined.
+my $root;
+my $testFile;
+my $showHelp;
+my $verbose;
+
+my $buildJSC = 1;
+
+my $programName = basename($0);
+my $buildJSCDefault = $buildJSC ? "will check" : "will not check";
+my $usage = <<EOF;
+Usage: $programName [options] [options to pass to build system]
+  --help                        Show this help message
+  --file=                       File to use instead of default ($defaultTestFile)
+  --root=                       Path to pre-built root containing jsc
+  --[no-]build                  Check (or don't check) to see if the jsc build is up-to-date (default: $buildJSCDefault)
+  --verbose                     Increase test output on failures
+EOF
+
+GetOptions(
+    'verbose' => \$verbose,
+    'root=s' => \$root,
+    'file=s' => \$testFile,
+    'build!' => \$buildJSC,
+    'help' => \$showHelp
+);
+
+# Assume any arguments left over from GetOptions are assumed to be build arguments
+my @buildArgs = @ARGV;
+
+
+if ($showHelp) {
+   print STDERR $usage;
+   exit 1;
+}
+
+setConfigurationProductDir(Cwd::abs_path($root)) if (defined($root));
+
+if (!defined($root) && $buildJSC) {
+    chdirWebKit();
+
+    push(@buildArgs, argumentsForConfiguration());
+    
+    print "Running: build-jsc " . join(" ", @buildArgs) . "\n";
+    my $buildResult = system "perl", "Tools/Scripts/build-jsc", @buildArgs;
+    if ($buildResult) {
+        print STDERR "Compiling jsc failed!\n";
+        exit exitStatus($buildResult);
+    }
+}
+
+my $productDir = jscProductDir();
+$ENV{DYLD_FRAMEWORK_PATH} = $productDir;
+setPathForRunningWebKitApp(\%ENV) if isCygwin();
+
+sub testapiPath($)
+{
+    my ($productDir) = @_;
+    my $jscName = "testapi";
+    $jscName .= "_debug" if configurationForVisualStudio() eq "Debug_All";
+    return "$productDir/$jscName";
+}
+
+# Find JavaScriptCore directory
+if (!defined($testFile)) {
+    $testFile = $defaultTestFile;
+    chdirWebKit();
+    chdir("Source/JavaScriptCore");
+    chdir "tests/regexp" or die;
+}
+
+my $command = $productDir . "/testRegExp";
+
+if (defined($verbose) && $verbose) {
+    $command .= " --verbose";
+}
+
+$command .= " " . $testFile;
+
+printf "Running: " . $command . "\n";
+my $result = system $command;
+exit exitStatus($result)  if $result;
+
diff --git a/Tools/Scripts/run-safari b/Tools/Scripts/run-safari
new file mode 100755
index 0000000..8688a93
--- /dev/null
+++ b/Tools/Scripts/run-safari
@@ -0,0 +1,43 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005, 2007 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 WebKit Open Source Project.
+
+use strict;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+printHelpAndExitForRunAndDebugWebKitAppIfNeeded();
+
+setConfiguration();
+
+# Check to see that all the frameworks are built.
+checkFrameworks();
+
+exit exitStatus(runSafari());
diff --git a/Tools/Scripts/run-sunspider b/Tools/Scripts/run-sunspider
new file mode 100755
index 0000000..1b60a75
--- /dev/null
+++ b/Tools/Scripts/run-sunspider
@@ -0,0 +1,138 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2007 Apple Inc. All rights reserved.
+# Copyright (C) 2007 Eric Seidel <eric@webkit.org>
+#
+# 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 COMPUTER, INC. ``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 COMPUTER, INC. OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
+
+use strict;
+use FindBin;
+use Getopt::Long qw(:config pass_through);
+use lib $FindBin::Bin;
+use webkitdirs;
+use POSIX;
+
+# determine configuration, but default to "Release" instead of last-used configuration
+setConfiguration("Release");
+setConfiguration();
+my $configuration = configuration();
+
+my $root;
+my $testRuns = 10; # This number may be different from what sunspider defaults to (that's OK)
+my $runShark = 0;
+my $runShark20 = 0;
+my $runSharkCache = 0;
+my $runInstruments = 0;
+my $suite = "";
+my $ubench = 0;
+my $v8suite = 0;
+my $parseonly = 0;
+my $setBaseline = 0;
+my $showHelp = 0;
+my $testsPattern;
+my $noBuild = 0;
+
+my $programName = basename($0);
+my $usage = <<EOF;
+Usage: $programName [options] [options to pass to build system]
+  --help            Show this help message
+  --set-baseline    Set baseline for future comparisons
+  --root            Path to root tools build
+  --runs            Number of times to run tests (default: $testRuns)
+  --tests           Only run tests matching provided pattern
+  --shark           Sample with the Mac OS X "Shark" performance testing tool (implies --runs=1)
+  --shark20         Like --shark, but with a 20 microsecond sampling interval
+  --shark-cache     Like --shark, but performs a L2 cache-miss sample instead of time sample
+  --instruments     Sample with the Mac OS X "Instruments" tool (Time Profile) (implies --runs=1)
+  --suite           Select a specific benchmark suite. The default is sunspider-0.9.1
+  --ubench          Use microbenchmark suite instead of regular tests. Same as --suite=ubench
+  --v8-suite        Use the V8 benchmark suite. Same as --suite=v8-v4
+  --parse-only      Use the parse-only benchmark suite. Same as --suite=parse-only
+  --no-build        Do not try to build JSC before running the tests.
+EOF
+
+GetOptions('root=s' => sub { my ($x, $value) = @_; $root = $value; setConfigurationProductDir(Cwd::abs_path($root)); },
+           'runs=i' => \$testRuns,
+           'set-baseline' => \$setBaseline,
+           'shark' => \$runShark,
+           'shark20' => \$runShark20,
+           'shark-cache' => \$runSharkCache,
+           'instruments' => \$runInstruments,
+           'suite=s' => \$suite,
+           'ubench' => \$ubench,
+           'v8-suite' => \$v8suite,
+           'parse-only' => \$parseonly,
+           'tests=s' => \$testsPattern,
+           'help' => \$showHelp,
+           'no-build' => \$noBuild);
+
+if ($showHelp) {
+   print STDERR $usage;
+   exit 1;
+}
+
+sub buildJSC
+{
+    if (!defined($root)){
+        push(@ARGV,  "--" . $configuration);
+        
+        chdirWebKit();
+        my $buildResult = system currentPerlPath(), "Tools/Scripts/build-jsc", @ARGV;
+        if ($buildResult) {
+            print STDERR "Compiling jsc failed!\n";
+            exit exitStatus($buildResult);
+        }
+    }
+}
+
+sub setupEnvironmentForExecution($)
+{
+    my ($productDir) = @_;
+    print "Starting sunspider with DYLD_FRAMEWORK_PATH set to point to built JavaScriptCore in $productDir.\n";
+    $ENV{DYLD_FRAMEWORK_PATH} = $productDir;
+    # FIXME: Other platforms may wish to augment this method to use LD_LIBRARY_PATH, etc.
+}
+
+unless ($noBuild) {
+    buildJSC();
+}
+
+chdirWebKit();
+chdir("PerformanceTests/SunSpider");
+
+my $productDir = jscProductDir();
+
+setupEnvironmentForExecution($productDir);
+my @args = ("--shell", jscPath($productDir), "--runs", $testRuns);
+# This code could be removed if we chose to pass extra args to sunspider instead of Xcode
+push @args, "--set-baseline" if $setBaseline;
+push @args, "--shark" if $runShark;
+push @args, "--shark20" if $runShark20;
+push @args, "--shark-cache" if $runSharkCache;
+push @args, "--instruments" if $runInstruments;
+push @args, "--suite=${suite}" if $suite;
+push @args, "--ubench" if $ubench;
+push @args, "--v8-suite" if $v8suite;
+push @args, "--parse-only" if $parseonly;
+push @args, "--tests", $testsPattern if $testsPattern;
+
+exec currentPerlPath(), "./sunspider", @args;
diff --git a/Tools/Scripts/run-test-runner b/Tools/Scripts/run-test-runner
new file mode 100755
index 0000000..b474eb5
--- /dev/null
+++ b/Tools/Scripts/run-test-runner
@@ -0,0 +1,37 @@
+#!/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.
+
+# Simplified "run" script for launching the WebKit2 WebKitTestRunner.
+
+use strict;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+printHelpAndExitForRunAndDebugWebKitAppIfNeeded();
+
+setConfiguration();
+
+exit exitStatus(runWebKitTestRunner());
diff --git a/Tools/Scripts/run-test-webkit-api b/Tools/Scripts/run-test-webkit-api
new file mode 100755
index 0000000..90b27f7
--- /dev/null
+++ b/Tools/Scripts/run-test-webkit-api
@@ -0,0 +1,40 @@
+#!/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 TestWebKitAPI.
+
+use strict;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+printHelpAndExitForRunAndDebugWebKitAppIfNeeded();
+
+setConfiguration();
+
+exit exitStatus(runTestWebKitAPI());
diff --git a/Tools/Scripts/run-webkit-app b/Tools/Scripts/run-webkit-app
new file mode 100755
index 0000000..d0c17f5
--- /dev/null
+++ b/Tools/Scripts/run-webkit-app
@@ -0,0 +1,46 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005 Apple Computer, Inc.  All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer. 
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution. 
+# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+#     its contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission. 
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Simplified "run" script for WebKit Open Source Project.
+
+use strict;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+printHelpAndExitForRunAndDebugWebKitAppIfNeeded();
+
+setConfiguration();
+
+die "Did not specify an application to open (e.g. run-webkit-app AppName).\n" unless length($ARGV[0]) > 0;
+
+# Check to see that all the frameworks are built.
+checkFrameworks();
+
+my $appPath = shift(@ARGV);
+exit exitStatus(runMacWebKitApp($appPath, USE_OPEN_COMMAND));
diff --git a/Tools/Scripts/run-webkit-httpd b/Tools/Scripts/run-webkit-httpd
new file mode 100755
index 0000000..8fb1887
--- /dev/null
+++ b/Tools/Scripts/run-webkit-httpd
@@ -0,0 +1,97 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2005, 2006, 2007 Apple Inc.  All rights reserved.
+# Copyright (C) 2006 Alexey Proskuryakov (ap@nypop.com)
+# Copyright (C) 2011 Research In Motion Limited. 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 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.
+
+# Script to run Apache with the same configuration as used in http layout tests.
+
+use strict;
+use warnings;
+
+use Cwd;
+use File::Path;
+use File::Basename;
+use Getopt::Long;
+use FindBin;
+
+use lib $FindBin::Bin;
+use webkitperl::httpd;
+use webkitdirs;
+
+# FIXME: Dynamic HTTP-port configuration in this file is wrong.  The various
+# apache config files in LayoutTests/http/config govern the port numbers.
+# Dynamic configuration as-written will also cause random failures in
+# an IPv6 environment.  See https://bugs.webkit.org/show_bug.cgi?id=37104.
+# Argument handling
+my $httpdPort = 8000;
+my $allInterfaces = 0;
+my $showHelp;
+
+my $result = GetOptions(
+    'all-interfaces|a' => \$allInterfaces,
+    'help|h' => \$showHelp,
+    'port=i' => \$httpdPort,
+);
+
+if (!$result || @ARGV || $showHelp) {
+    print "Usage: " . basename($0) . " [options]\n";
+    print "  -a|--all-interfaces  Bind to all interfaces\n";
+    print "  -h|--help            Show this help message\n";
+    print "  -p|--port NNNN       Bind to port NNNN\n";
+    exit 1;
+}
+
+setConfiguration();
+my $productDir = productDir();
+chdirWebKit();
+my $testDirectory = File::Spec->catfile(getcwd(), "LayoutTests");
+my $listen = "127.0.0.1:$httpdPort";
+$listen = "$httpdPort" if ($allInterfaces);
+
+if ($allInterfaces) {
+    print "Starting httpd on port $httpdPort (all interfaces)...\n";
+} else {
+    print "Starting httpd on <http://$listen/>...\n";
+}
+setShouldWaitForUserInterrupt();
+print "Press Ctrl+C to stop it.\n\n";
+
+my @args = (
+    "-C", "Listen $listen",
+    "-c", "CustomLog |/usr/bin/tee common",
+    "-c", "ErrorLog |/usr/bin/tee",
+    # Run in single-process mode, do not detach from the controlling terminal.
+    "-X",
+    # Disable Keep-Alive support. Makes testing in multiple browsers easier (no need to wait
+    # for another browser's connection to expire).
+    "-c", "KeepAlive 0"
+);
+
+my @defaultArgs = getDefaultConfigForTestDirectory($testDirectory);
+@args = (@defaultArgs, @args);
+openHTTPD(@args);
diff --git a/Tools/Scripts/run-webkit-tests b/Tools/Scripts/run-webkit-tests
new file mode 100755
index 0000000..4bb8f39
--- /dev/null
+++ b/Tools/Scripts/run-webkit-tests
@@ -0,0 +1,126 @@
+#!/usr/bin/perl
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# This file is a temporary hack.
+# It will be removed as soon as all platforms are are ready to move to
+# new-run-webkit-tests and we can then update the buildbots to explicitly
+# call old-run-webkit-tests for any platforms which will never support
+# a Python run-webkit-tests.
+
+# This is intentionally written in Perl to guarantee support on
+# the same set of platforms as old-run-webkit-tests currently supports.
+# The buildbot master.cfg also currently passes run-webkit-tests to
+# perl directly instead of executing it in a shell.
+
+use strict;
+use warnings;
+
+use File::Spec;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+sub runningOnBuildBot()
+{
+    # This is a hack to detect if we're running on the buildbot so we can
+    # pass --verbose to new-run-webkit-tests.  This will be removed when we
+    # update the buildbot config to call new-run-webkit-tests explicitly.
+    my %isBuildBotUser = ("apple" => 1, "buildbot" => 1, "webkitbuildbot" => 1, "slave" => 1, "buildslave-1" => 1, "chrome-bot" => 1);
+    return $isBuildBotUser{$ENV{"USER"}};
+}
+
+sub useNewRunWebKitTests()
+{
+    # NRWT Windows support still needs work: https://bugs.webkit.org/show_bug.cgi?id=38756
+    return 0 if (isWindows() or isCygwin()) and !isChromium();
+    # NRWT does not support qt-arm: https://bugs.webkit.org/show_bug.cgi?id=64086
+    return 0 if isQt() and isARM();
+    # All other platforms should use NRWT by default.
+    return 1;
+}
+
+sub platformIsReadyForParallelTesting()
+{
+    # NRWT is able to run the tests in parallel, ORWT was not.
+    # When we run the tests in parallel, tests which (incorrectly)
+    # interact with each other can start failing.
+    # To reduce the failure burden during the transition individual
+    # platforms can opt-in to parallel test execution by default.
+
+    # We believe all platforms are ready for default parallel testing except
+    # Qt, as Qt runs more than one build-slave per-server.
+    # Ossy has asked me to blacklist Qt for now.
+    return !isQt();
+}
+
+my $script = "perl";
+my $harnessName = "old-run-webkit-tests";
+
+if (useNewRunWebKitTests()) {
+    $script = "python";
+    $harnessName = "new-run-webkit-tests";
+
+    if (!grep(/--child-processes/, @ARGV) and !platformIsReadyForParallelTesting()) {
+        push(@ARGV, "--child-processes=1");
+        print "Running new-run-webkit-tests with one child process.\n";
+        print "For more parallelism, run new-run-webkit-tests directly.\n";
+    }
+
+    if (runningOnBuildBot()) {
+        push(@ARGV, "--debug-rwt-logging");
+    }
+}
+
+# webkitdirs.pm strips --qt and --gtk from @ARGV when we call isQt/isGtk.
+# We have to add back any --PORT arguments which may have been removed by isPort() checks above.
+if (isQt()) {
+    my $isPlatformSet = 0;
+    for (@ARGV){
+        # Pass --qt if platform isn't passed explicitly (eg. qt-5.0, qt-wk2, ...)
+        if(/^--platform.*/){
+            $isPlatformSet = 1;
+        }
+    }
+    push(@ARGV, "--qt") if(!$isPlatformSet);
+} elsif (isGtk()) {
+    push(@ARGV, "--gtk");
+} elsif (isEfl()) {
+    push(@ARGV, "--efl");
+} elsif (isChromiumAndroid()) {
+    push(@ARGV, "--chromium-android");
+} elsif (isChromium()) {
+    push(@ARGV, "--chromium");
+} elsif (isWinCairo()) {
+    push(@ARGV, "--wincairo");
+}
+
+my $harnessPath = File::Spec->catfile(relativeScriptsDir(), $harnessName);
+unshift(@ARGV, $harnessPath);
+unshift(@ARGV, $script);
+system(@ARGV) == 0 or die "Failed to execute $harnessPath";
diff --git a/Tools/Scripts/run-webkit-websocketserver b/Tools/Scripts/run-webkit-websocketserver
new file mode 100755
index 0000000..d030951
--- /dev/null
+++ b/Tools/Scripts/run-webkit-websocketserver
@@ -0,0 +1,88 @@
+#!/usr/bin/perl
+# Copyright (C) 2009 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.
+
+# Script to run Web Socket server.
+
+use strict;
+use warnings;
+
+use File::Spec;
+use FindBin;
+use IPC::Open2;
+
+use lib $FindBin::Bin;
+use webkitdirs;
+
+sub closeWebSocketServer();
+sub openWebSocketServer();
+
+my $webSocketPort = 8880;
+
+my $srcDir = sourceDir();
+my $layoutTestsName = "$srcDir/LayoutTests";
+my $testDirectory = File::Spec->rel2abs($layoutTestsName);
+my $webSocketServerPidFile = "$testDirectory/websocket.pid";
+
+
+print "Starting Web Socket server...\n";
+openWebSocketServer();
+print "Started.\n";
+print "Hit [ENTER] to stop it.";
+<STDIN>;
+print "Stopping Web Socket server...\n";
+closeWebSocketServer();
+print "Stopped.\n";
+exit 0;
+
+sub openWebSocketServer()
+{
+    my $webSocketHandlerDir = "$testDirectory";
+
+    my @args = (
+        "$srcDir/Tools/Scripts/new-run-webkit-websocketserver",
+        "--server", "start",
+        "--port", "$webSocketPort",
+        "--root", "$webSocketHandlerDir",
+        "--pidfile", "$webSocketServerPidFile"
+    );
+    system "/usr/bin/python", @args;
+}
+
+sub closeWebSocketServer()
+{
+    my @args = (
+        "$srcDir/Tools/Scripts/new-run-webkit-websocketserver",
+        "--server", "stop",
+        "--pidfile", "$webSocketServerPidFile"
+    );
+    system "/usr/bin/python", @args;
+    unlink "$webSocketServerPidFile";
+}
+
+
diff --git a/Tools/Scripts/set-webkit-configuration b/Tools/Scripts/set-webkit-configuration
new file mode 100755
index 0000000..4992256
--- /dev/null
+++ b/Tools/Scripts/set-webkit-configuration
@@ -0,0 +1,79 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005 Apple Computer, Inc.  All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer. 
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution. 
+# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+#     its contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission. 
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+use strict;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+my $programName = basename($0);
+my $usage = <<EOF;
+Usage: $programName [options]
+  --32-bit                Set the default architecture to 32-bit
+  --64-bit                Set the default architecture to 64-bit
+  --debug                 Set the default configuration to debug
+  --release               Set the default configuration to release
+EOF
+
+my $configuration = passedConfiguration();
+my $architecture = passedArchitecture();
+
+if (!$architecture) {
+    # Handle --64-bit explicitly here, as we don't want our other scripts to accept it
+    for my $i (0 .. $#ARGV) {
+        my $opt = $ARGV[$i];
+        if ($opt =~ /^--64-bit$/i) {
+            splice(@ARGV, $i, 1);
+            $architecture = 'x86_64';
+        }
+    }
+}
+
+if (!$configuration && !$architecture) {
+    print STDERR $usage;
+    exit 1;
+}
+
+my $baseProductDir = baseProductDir();
+system "mkdir", "-p", "$baseProductDir";
+
+if ($configuration) {
+    open CONFIGURATION, ">", "$baseProductDir/Configuration" or die;
+    print CONFIGURATION $configuration;
+    close CONFIGURATION;
+}
+
+if ($architecture) {
+    if ($architecture ne "x86_64") {
+        open ARCHITECTURE, ">", "$baseProductDir/Architecture" or die;
+        print ARCHITECTURE $architecture;
+        close ARCHITECTURE;
+    } else {
+        unlink "$baseProductDir/Architecture";
+    }
+}
diff --git a/Tools/Scripts/show-pretty-diff b/Tools/Scripts/show-pretty-diff
new file mode 100755
index 0000000..be426c0
--- /dev/null
+++ b/Tools/Scripts/show-pretty-diff
@@ -0,0 +1,79 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2011 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 FindBin;
+use File::Temp qw(tempfile);
+use lib $FindBin::Bin;
+use webkitdirs;
+
+my $inputPath = "";
+if ($ARGV[0]) {
+    $inputPath = $ARGV[0]
+} else {
+    # Create a temporary file for STDIN.
+    # FIXME: We can probably avoid putting this on the disk by directly piping
+    # to prettify.rb via IPC::Open2.
+    my $inputTempFileHandle;
+    ($inputTempFileHandle, $inputPath) = tempfile(
+        "inputtemp-XXXXXXXX",
+        DIR => File::Spec->tmpdir(),
+        SUFFIX => ".diff",
+        UNLINK => 0,
+    );
+
+    while (<STDIN>) {
+        print $inputTempFileHandle $_;
+    }
+
+    close($inputTempFileHandle);
+}
+
+# Create a temporary file for prettified patch.
+my ($prettydiffFileHandle, $prettydiffPath) = tempfile(
+    "prettydiff-XXXXXXXX",
+    DIR => File::Spec->tmpdir(),
+    SUFFIX => ".html",
+    UNLINK => 0,
+);
+close($prettydiffFileHandle);
+
+my $prettyPatchDir = sourceDir() . "/Websites/bugs.webkit.org/PrettyPatch/";
+my $prettyPatchTool = sourceDir() . "/Websites/bugs.webkit.org/PrettyPatch/prettify.rb";
+
+my $pathToPrettify = "ruby -I " . sourceDir() . "/Websites/bugs.webkit.org/PrettyPatch/ " . sourceDir() . "/Websites/bugs.webkit.org/PrettyPatch/prettify.rb";
+system "$pathToPrettify " . quotemeta($inputPath) . " > $prettydiffPath";
+
+if (isAppleMacWebKit()) {
+    system "open", $prettydiffPath;
+} elsif (isCygwin()) {
+    system "cygstart",$prettydiffPath;
+} elsif (isWindows()) {
+    system "start", $prettydiffPath;
+} elsif (isLinux() && `which xdg-open`) {
+    system "xdg-open", $prettydiffPath;
+} else {
+    print "Created prettified diff at " . $prettydiffPath . ".";
+}
diff --git a/Tools/Scripts/sort-Xcode-project-file b/Tools/Scripts/sort-Xcode-project-file
new file mode 100755
index 0000000..705b41d
--- /dev/null
+++ b/Tools/Scripts/sort-Xcode-project-file
@@ -0,0 +1,172 @@
+#!/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. 
+# 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.
+
+# Script to sort "children" and "files" sections in Xcode project.pbxproj files
+
+use strict;
+
+use File::Basename;
+use File::Spec;
+use File::Temp qw(tempfile);
+use Getopt::Long;
+
+sub sortChildrenByFileName($$);
+sub sortFilesByFileName($$);
+
+# Files (or products) without extensions
+my %isFile = map { $_ => 1 } qw(
+    create_hash_table
+    jsc
+    minidom
+    testapi
+    testjsglue
+);
+
+my $printWarnings = 1;
+my $showHelp;
+
+my $getOptionsResult = GetOptions(
+    '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 ...]
+  -h|--help           show this help message
+  -w|--[no-]warnings  show or suppress warnings (default: show warnings)
+__END__
+    exit 1;
+}
+
+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;
+    }
+
+    # Grab the mainGroup for the project file
+    my $mainGroup = "";
+    open(IN, "< $projectFile") || die "Could not open $projectFile: $!";
+    while (my $line = <IN>) {
+        $mainGroup = $2 if $line =~ m#^(\s*)mainGroup = ([0-9A-F]{24} /\* .+ \*/);$#;
+    }
+    close(IN);
+
+    my ($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);
+    };
+
+    my @lastTwo = ();
+    open(IN, "< $projectFile") || die "Could not open $projectFile: $!";
+    while (my $line = <IN>) {
+        if ($line =~ /^(\s*)files = \(\s*$/) {
+            print $OUT $line;
+            my $endMarker = $1 . ");";
+            my @files;
+            while (my $fileLine = <IN>) {
+                if ($fileLine =~ /^\Q$endMarker\E\s*$/) {
+                    $endMarker = $fileLine;
+                    last;
+                }
+                push @files, $fileLine;
+            }
+            print $OUT sort sortFilesByFileName @files;
+            print $OUT $endMarker;
+        } elsif ($line =~ /^(\s*)children = \(\s*$/) {
+            print $OUT $line;
+            my $endMarker = $1 . ");";
+            my @children;
+            while (my $childLine = <IN>) {
+                if ($childLine =~ /^\Q$endMarker\E\s*$/) {
+                    $endMarker = $childLine;
+                    last;
+                }
+                push @children, $childLine;
+            }
+            if ($lastTwo[0] =~ m#^\s+\Q$mainGroup\E = \{$#) {
+                # Don't sort mainGroup
+                print $OUT @children;
+            } else {
+                print $OUT sort sortChildrenByFileName @children;
+            }
+            print $OUT $endMarker;
+        } else {
+            print $OUT $line;
+        }
+
+        push @lastTwo, $line;
+        shift @lastTwo if scalar(@lastTwo) > 2;
+    }
+    close(IN);
+    close($OUT);
+
+    unlink($projectFile) || die "Could not delete $projectFile: $!";
+    rename($tempFileName, $projectFile) || die "Could not rename $tempFileName to $projectFile: $!";
+}
+
+exit 0;
+
+sub sortChildrenByFileName($$)
+{
+    my ($a, $b) = @_;
+    my $aFileName = $1 if $a =~ /^\s*[A-Z0-9]{24} \/\* (.+) \*\/,$/;
+    my $bFileName = $1 if $b =~ /^\s*[A-Z0-9]{24} \/\* (.+) \*\/,$/;
+    my $aSuffix = $1 if $aFileName =~ m/\.([^.]+)$/;
+    my $bSuffix = $1 if $bFileName =~ m/\.([^.]+)$/;
+    if ((!$aSuffix && !$isFile{$aFileName} && $bSuffix) || ($aSuffix && !$bSuffix && !$isFile{$bFileName})) {
+        return !$aSuffix ? -1 : 1;
+    }
+    return lc($aFileName) cmp lc($bFileName);
+}
+
+sub sortFilesByFileName($$)
+{
+    my ($a, $b) = @_;
+    my $aFileName = $1 if $a =~ /^\s*[A-Z0-9]{24} \/\* (.+) in /;
+    my $bFileName = $1 if $b =~ /^\s*[A-Z0-9]{24} \/\* (.+) in /;
+    return lc($aFileName) cmp lc($bFileName);
+}
diff --git a/Tools/Scripts/split-file-by-class b/Tools/Scripts/split-file-by-class
new file mode 100755
index 0000000..b6aeb68
--- /dev/null
+++ b/Tools/Scripts/split-file-by-class
@@ -0,0 +1,159 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2006 Apple Computer, Inc.  All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer. 
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution. 
+# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+#     its contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission. 
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Used for splitting a single file into multiple class files
+# Usage: split-class <header file>
+
+use strict;
+use File::Copy;
+use FindBin;
+use lib $FindBin::Bin;
+use SpacingHeuristics;
+
+
+for my $filename (@ARGV) {
+
+    $filename =~ m/^(\w+)\.h$/ or die "Command line args must be .h files.\n";
+    my $basename = $1;
+
+    open(OLDFILE, "<", $filename) or die "File does not exist: $filename\n";
+    print "Splitting class $filename.{h,cpp}:\n";
+    
+    my $currentClassName = "";
+    my $classIndent = "";
+    my $fileContent = "";
+    my %classDefs = ();
+    while (my $line = <OLDFILE>) {
+        if ($currentClassName) {
+            $classDefs{$currentClassName} .= $line;
+            if ($line =~ /^$classIndent};\s*$/) {
+                $currentClassName = "";
+            }
+        } else {
+            if ($line =~ /^(\s*)class\s+(\w+)\s+[^;]*$/) {
+                $classIndent = $1;
+                $currentClassName = $2;
+                $classDefs{$currentClassName} .= $line;
+                $fileContent .= "###CLASS###$currentClassName\n";
+            } else {
+                $fileContent .= $line;
+            }
+        }
+    }
+    close(OLDFILE);
+    
+    if (scalar(keys(%classDefs)) == 1) { # degenerate case
+        my ($classname) = keys(%classDefs);
+        if (!($classname eq $basename)) {
+            print "Skipping $filename, already correctly named.\n";
+        } else {
+            print "$filename only includes one class, renaming to $classname.h\n";
+            system("svn rm --force $classname.h") if (-r "$classname.h");
+            system "svn mv $basename.h $classname.h";
+        }
+    } else {
+        while (my ($classname, $classDef) = each(%classDefs)) {
+            if (($classname eq $basename)) {
+                print "Skipping $filename, already correctly named.\n";
+            } else {
+                print "Using SVN to copy $basename.{h,cpp} to $classname.{h,cpp}\n";
+                
+                system("svn rm --force $classname.h") if (-r "$classname.h");
+                system "svn cp $basename.h $classname.h";
+                
+                system("svn rm --force $classname.cpp") if (-r "$classname.cpp");
+                system "svn cp $basename.cpp $classname.cpp";
+            }
+            
+            print "Fixing $classname.h as much as possible.\n";
+            open(NEWHEADER, ">", "$classname.h") or die "File does not exist: $filename\n";
+            my @lines = split("\n", $fileContent);
+            foreach my $line (@lines) {
+                if ($line =~ /^###CLASS###(\w+)/) {
+                    if ($1 eq $classname) {
+                        print NEWHEADER $classDef . "\n";
+                    }
+                } else {
+                    print NEWHEADER $line . "\n";
+                }
+            }
+            close(NEWHEADER);
+            
+            print "Fixing $classname.cpp as much as possible.\n";
+            copy("$classname.cpp", "$classname.cpp.original");
+            open(OLDCPP, "<", "$classname.cpp.original") or die "Failed to copy file for reading: $filename\n";
+            open(NEWCPP, ">", "$classname.cpp") or die "File does not exist: $filename\n";
+            my $insideMemberFunction = 0;
+            my $shouldPrintMemberFunction = 0;
+            resetSpacingHeuristics();
+            while (my $line = <OLDCPP>) {
+                if ($insideMemberFunction) {
+                    if ($shouldPrintMemberFunction) {
+                        print NEWCPP $line;
+                        #setPreviousAllowedLine($line);
+                    } else {
+                        ignoringLine($line);
+                    }
+                    if ($line =~ /^}\s*$/) {
+                        $insideMemberFunction = 0;
+                    }
+                } elsif ($line =~ /$filename/) {
+                    print NEWCPP "#include \"config.h\"\n";
+                    print NEWCPP "#include \"$classname.h\"\n";
+                } elsif ($line =~ /#include/ || $line =~ /#import/) {
+                    next; # skip includes, they're generally wrong or unecessary anyway.
+                } else {
+                    $line =~ s/DOM:://;
+                    $line =~ s/khtml:://;
+                    $line =~ s/namespace DOM/namespace WebCore/;
+                    $line =~ s/namespace khtml/namespace WebCore/;
+                    
+                    if ($line =~ /^(.*?\s+)?(\*|&)?(\w+)::(~)?\w+\s*\(/) {
+                        $insideMemberFunction = 1;
+                        $shouldPrintMemberFunction = ($classname eq $3);
+                        if ($shouldPrintMemberFunction) {
+                            printPendingEmptyLines(*NEWCPP, $line);
+                            print NEWCPP $line;
+                        }
+                    } else {
+                        next if isOnlyWhiteSpace($line);
+                        next if ($line =~ m/------------/);
+                        printPendingEmptyLines(*NEWCPP, $line);
+                        applySpacingHeuristicsAndPrint(*NEWCPP, $line);
+                    }
+                }
+            }
+            close(NEWCPP);
+            close(OLDCPP);
+            unlink("$classname.cpp.original");
+        }
+    }
+    
+    print "Opening new files...\n";
+    system("open " . join(".* ", keys(%classDefs)) . ".*");
+}
\ No newline at end of file
diff --git a/Tools/Scripts/sunspider-compare-results b/Tools/Scripts/sunspider-compare-results
new file mode 100755
index 0000000..ce95944
--- /dev/null
+++ b/Tools/Scripts/sunspider-compare-results
@@ -0,0 +1,133 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2007 Apple Inc. All rights reserved.
+# Copyright (C) 2007 Eric Seidel <eric@webkit.org>
+#
+# 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 COMPUTER, INC. ``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 COMPUTER, INC. OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
+
+use strict;
+use File::Spec;
+use FindBin;
+use Getopt::Long qw(:config pass_through);
+use lib $FindBin::Bin;
+use webkitdirs;
+use POSIX;
+
+# determine configuration, but default to "Release" instead of last-used configuration to match run-sunspider
+setConfiguration("Release");
+setConfiguration();
+my $configuration = configuration();
+
+my $root;
+my $showHelp = 0;
+my $suite = "";
+my $ubench = 0;
+my $v8 = 0;
+my $parseonly = 0;
+
+my $programName = basename($0);
+my $usage = <<EOF;
+Usage: $programName [options] FILE FILE
+  --help        Show this help message
+  --root        Path to root tools build
+  --suite           Select a specific benchmark suite. The default is sunspider-0.9.1
+  --ubench          Use microbenchmark suite instead of regular tests. Same as --suite=ubench
+  --v8-suite        Use the V8 benchmark suite. Same as --suite=v8-v4
+  --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)); $root = $value; },
+           'suite=s' => \$suite,
+           'ubench' => \$ubench,
+           'v8' => \$v8,
+           'parse-only' => \$parseonly,
+           'help' => \$showHelp);
+
+if ($showHelp) {
+   print STDERR $usage;
+   exit 1;
+}
+
+sub buildJSC
+{
+    if (!defined($root)){
+        chdirWebKit();
+        my $buildResult = system currentPerlPath(), "Tools/Scripts/build-jsc", "--" . $configuration;
+        if ($buildResult) {
+            print STDERR "Compiling jsc failed!\n";
+            exit WEXITSTATUS($buildResult);
+        }
+    }
+}
+
+sub setupEnvironmentForExecution($)
+{
+    my ($productDir) = @_;
+    print "Starting sunspider-compare-results with DYLD_FRAMEWORK_PATH set to point to built JavaScriptCore in $productDir.\n";
+    $ENV{DYLD_FRAMEWORK_PATH} = $productDir;
+    # FIXME: Other platforms may wish to augment this method to use LD_LIBRARY_PATH, etc.
+}
+
+sub pathToBuiltJSC($)
+{
+    my ($productDir) = @_;
+    my $jscName = "jsc";
+    $jscName .= "_debug" if configurationForVisualStudio() eq "Debug_All";
+    return "$productDir/$jscName";
+}
+
+sub pathToSystemJSC()
+{
+    my $path = "/System/Library/Frameworks/JavaScriptCore.framework/Resources/jsc";
+    if (-f $path) {
+        return $path;
+    }
+    return undef;
+}
+
+sub pathToJSC()
+{
+    my $path = pathToSystemJSC();
+    return $path if defined $path;
+
+    buildJSC();
+
+    my $productDir = jscProductDir();
+
+    setupEnvironmentForExecution($productDir);
+    return pathToBuiltJSC($productDir);
+}
+
+my $jscPath = pathToJSC();
+chdirWebKit();
+chdir("PerformanceTests/SunSpider");
+
+my @args = ("--shell", $jscPath);
+# This code could be removed if we chose to pass extra args to sunspider instead of Xcode
+push @args, "--suite=${suite}" if $suite;
+push @args, "--ubench" if $ubench;
+push @args, "--v8" if $v8;
+push @args, "--parse-only" if $parseonly;
+
+@ARGV = map { File::Spec->rel2abs($_) } @ARGV;
+
+exec currentPerlPath(), "./sunspider-compare-results", @args, @ARGV;
diff --git a/Tools/Scripts/svn-apply b/Tools/Scripts/svn-apply
new file mode 100755
index 0000000..fb52a9b
--- /dev/null
+++ b/Tools/Scripts/svn-apply
@@ -0,0 +1,475 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005, 2006, 2007 Apple Inc.  All rights reserved.
+# Copyright (C) 2009 Cameron McCormack <cam@mcc.id.au>
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+#
+# 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.
+
+# "patch" script for WebKit Open Source Project, used to apply patches.
+
+# Differences from invoking "patch -p0":
+#
+#   Handles added files (does a svn add with logic to handle local changes).
+#   Handles added directories (does a svn add).
+#   Handles removed files (does a svn rm with logic to handle local changes).
+#   Handles removed directories--those with no more files or directories left in them
+#       (does a svn rm).
+#   Has mode where it will roll back to svn version numbers in the patch file so svn
+#       can do a 3-way merge.
+#   Paths from Index: lines are used rather than the paths on the patch lines, which
+#       makes patches generated by "cvs diff" work (increasingly unimportant since we
+#       use Subversion now).
+#   ChangeLog patches use --fuzz=3 to prevent rejects.
+#   Handles binary files (requires patches made by svn-create-patch).
+#   Handles copied and moved files (requires patches made by svn-create-patch).
+#   Handles git-diff patches (without binary changes) created at the top-level directory
+#
+# Missing features:
+#
+#   Handle property changes.
+#   Handle copied and moved directories (would require patches made by svn-create-patch).
+#   When doing a removal, check that old file matches what's being removed.
+#   Notice a patch that's being applied at the "wrong level" and make it work anyway.
+#   Do a dry run on the whole patch and don't do anything if part of the patch is
+#       going to fail (probably too strict unless we exclude ChangeLog).
+#   Handle git-diff patches with binary delta
+
+use strict;
+use warnings;
+
+use Digest::MD5;
+use File::Basename;
+use File::Spec;
+use Getopt::Long;
+use MIME::Base64;
+use POSIX qw(strftime);
+
+use FindBin;
+use lib $FindBin::Bin;
+use VCSUtils;
+
+sub addDirectoriesIfNeeded($);
+sub applyPatch($$;$);
+sub checksum($);
+sub handleBinaryChange($$);
+sub handleGitBinaryChange($$);
+sub isDirectoryEmptyForRemoval($);
+sub patch($);
+sub removeDirectoriesIfNeeded();
+
+# These should be replaced by an scm class/module:
+sub scmKnowsOfFile($);
+sub scmCopy($$);
+sub scmAdd($);
+sub scmRemove($);
+
+my $merge = 0;
+my $showHelp = 0;
+my $reviewer;
+my $force = 0;
+
+my $optionParseSuccess = GetOptions(
+    "merge!" => \$merge,
+    "help!" => \$showHelp,
+    "reviewer=s" => \$reviewer,
+    "force!" => \$force
+);
+
+if (!$optionParseSuccess || $showHelp) {
+    print STDERR basename($0) . " [-h|--help] [--force] [-m|--merge] [-r|--reviewer name] patch1 [patch2 ...]\n";
+    exit 1;
+}
+
+my %removeDirectoryIgnoreList = (
+    '.' => 1,
+    '..' => 1,
+    '.git' => 1,
+    '.svn' => 1,
+    '_svn' => 1,
+);
+
+my $epochTime = time(); # This is used to set the date in ChangeLog files.
+my $globalExitStatus = 0;
+
+my $repositoryRootPath = determineVCSRoot();
+
+my %checkedDirectories;
+
+# Need to use a typeglob to pass the file handle as a parameter,
+# otherwise get a bareword error.
+my @diffHashRefs = parsePatch(*ARGV);
+
+print "Parsed " . @diffHashRefs . " diffs from patch file(s).\n";
+
+my $preparedPatchHash = prepareParsedPatch($force, @diffHashRefs);
+
+my @copyDiffHashRefs = @{$preparedPatchHash->{copyDiffHashRefs}};
+my @nonCopyDiffHashRefs = @{$preparedPatchHash->{nonCopyDiffHashRefs}};
+my %sourceRevisions = %{$preparedPatchHash->{sourceRevisionHash}};
+
+if ($merge) {
+    die "--merge is currently only supported for SVN" unless isSVN();
+    # How do we handle Git patches applied to an SVN checkout here?
+    for my $file (sort keys %sourceRevisions) {
+        my $version = $sourceRevisions{$file};
+        print "Getting version $version of $file\n";
+        my $escapedFile = escapeSubversionPath($file);
+        system("svn", "update", "-r", $version, $escapedFile) == 0 or die "Failed to run svn update -r $version $escapedFile.";
+    }
+}
+
+# Handle copied and moved files first since moved files may have their
+# source deleted before the move.
+for my $copyDiffHashRef (@copyDiffHashRefs) {
+    my $indexPath = $copyDiffHashRef->{indexPath};
+    my $copiedFromPath = $copyDiffHashRef->{copiedFromPath};
+
+    addDirectoriesIfNeeded(dirname($indexPath));
+    scmCopy($copiedFromPath, $indexPath);
+}
+
+for my $diffHashRef (@nonCopyDiffHashRefs) {
+    patch($diffHashRef);
+}
+
+removeDirectoriesIfNeeded();
+
+exit $globalExitStatus;
+
+sub addDirectoriesIfNeeded($)
+{
+    # Git removes a directory once the last file in it is removed. We need
+    # explicitly check for the existence of each directory along the path
+    # (and create it if it doesn't) so as to support patches that move all files in
+    # directory A to A/B. That is, we cannot depend on %checkedDirectories.
+    my ($path) = @_;
+    my @dirs = File::Spec->splitdir($path);
+    my $dir = ".";
+    while (scalar @dirs) {
+        $dir = File::Spec->catdir($dir, shift @dirs);
+        next if !isGit() && exists $checkedDirectories{$dir};
+        if (! -e $dir) {
+            mkdir $dir or die "Failed to create required directory '$dir' for path '$path'\n";
+            scmAdd($dir);
+            $checkedDirectories{$dir} = 1;
+        }
+        elsif (-d $dir) {
+            # SVN prints "svn: warning: 'directory' is already under version control"
+            # if you try and add a directory which is already in the repository.
+            # Git will ignore the add, but re-adding large directories can be sloooow.
+            # So we check first to see if the directory is under version control first.
+            if (!scmKnowsOfFile($dir)) {
+                scmAdd($dir);
+            }
+            $checkedDirectories{$dir} = 1;
+        }
+        else {
+            die "'$dir' exists, but is not a directory";
+        }
+    }
+}
+
+# Args:
+#   $patch: a patch string.
+#   $pathRelativeToRoot: the path of the file to be patched, relative to the
+#                        repository root. This should normally be the path
+#                        found in the patch's "Index:" line.
+#   $options: a reference to an array of options to pass to the patch command.
+sub applyPatch($$;$)
+{
+    my ($patch, $pathRelativeToRoot, $options) = @_;
+
+    my $optionalArgs = {options => $options, ensureForce => $force};
+
+    my $exitStatus = runPatchCommand($patch, $repositoryRootPath, $pathRelativeToRoot, $optionalArgs);
+
+    if ($exitStatus) {
+        $globalExitStatus = $exitStatus;
+    }
+}
+
+sub checksum($)
+{
+    my $file = shift;
+    open(FILE, $file) or die "Can't open '$file': $!";
+    binmode(FILE);
+    my $checksum = Digest::MD5->new->addfile(*FILE)->hexdigest();
+    close(FILE);
+    return $checksum;
+}
+
+sub handleBinaryChange($$)
+{
+    my ($fullPath, $contents) = @_;
+    # [A-Za-z0-9+/] is the class of allowed base64 characters.
+    # One or more lines, at most 76 characters in length.
+    # The last line is allowed to have up to two '=' characters at the end (to signify padding).
+    if ($contents =~ m#((\n[A-Za-z0-9+/]{76})*\n[A-Za-z0-9+/]{2,74}?[A-Za-z0-9+/=]{2}\n)#) {
+        # Addition or Modification
+        open FILE, ">", $fullPath or die "Failed to open $fullPath.";
+        print FILE decode_base64($1);
+        close FILE;
+        if (!scmKnowsOfFile($fullPath)) {
+            # Addition
+            scmAdd($fullPath);
+        }
+    } else {
+        # Deletion
+        scmRemove($fullPath);
+    }
+}
+
+sub handleGitBinaryChange($$)
+{
+    my ($fullPath, $diffHashRef) = @_;
+
+    my $contents = $diffHashRef->{svnConvertedText};
+
+    my ($binaryChunkType, $binaryChunk, $reverseBinaryChunkType, $reverseBinaryChunk) = decodeGitBinaryPatch($contents, $fullPath);
+
+    my $isFileAddition = $diffHashRef->{isNew};
+    my $isFileDeletion = $diffHashRef->{isDeletion};
+
+    my $originalContents = "";
+    if (open FILE, $fullPath) {
+        die "$fullPath already exists" if $isFileAddition;
+
+        $originalContents = join("", <FILE>);
+        close FILE;
+    }
+
+    if ($reverseBinaryChunkType eq "literal") {
+        die "Original content of $fullPath mismatches" if $originalContents ne $reverseBinaryChunk;
+    }
+
+    if ($isFileDeletion) {
+        scmRemove($fullPath);
+    } else {
+        # Addition or Modification
+        my $out = "";
+        if ($binaryChunkType eq "delta") {
+            $out = applyGitBinaryPatchDelta($binaryChunk, $originalContents);
+        } else {
+            $out = $binaryChunk;
+        }
+        if ($reverseBinaryChunkType eq "delta") {
+            die "Original content of $fullPath mismatches" if $originalContents ne applyGitBinaryPatchDelta($reverseBinaryChunk, $out);
+        }
+        open FILE, ">", $fullPath or die "Failed to open $fullPath.";
+        print FILE $out;
+        close FILE;
+        if ($isFileAddition) {
+            scmAdd($fullPath);
+        }
+    }
+}
+
+sub isDirectoryEmptyForRemoval($)
+{
+    my ($dir) = @_;
+    return 1 unless -d $dir;
+    my $directoryIsEmpty = 1;
+    opendir DIR, $dir or die "Could not open '$dir' to list files: $?";
+    for (my $item = readdir DIR; $item && $directoryIsEmpty; $item = readdir DIR) {
+        next if exists $removeDirectoryIgnoreList{$item};
+        if (-d File::Spec->catdir($dir, $item)) {
+            $directoryIsEmpty = 0;
+        } else {
+            next if (scmWillDeleteFile(File::Spec->catdir($dir, $item)));
+            $directoryIsEmpty = 0;
+        }
+    }
+    closedir DIR;
+    return $directoryIsEmpty;
+}
+
+# Args:
+#   $diffHashRef: a diff hash reference of the type returned by parsePatch().
+sub patch($)
+{
+    my ($diffHashRef) = @_;
+
+    # Make sure $patch is initialized to some value.  A deletion can have no
+    # svnConvertedText property in the case of a deletion resulting from a
+    # Git rename.
+    my $patch = $diffHashRef->{svnConvertedText} || "";
+
+    my $fullPath = $diffHashRef->{indexPath};
+    my $isBinary = $diffHashRef->{isBinary};
+    my $isGit = $diffHashRef->{isGit};
+    my $hasTextChunks = $patch && $diffHashRef->{numTextChunks};
+
+    my $deletion = 0;
+    my $addition = 0;
+
+    $addition = 1 if ($diffHashRef->{isNew} || $patch =~ /\n@@ -0,0 .* @@/);
+    $deletion = 1 if ($diffHashRef->{isDeletion} || $patch =~ /\n@@ .* \+0,0 @@/);
+
+    if (!$addition && !$deletion && !$isBinary && $hasTextChunks) {
+        # Standard patch, patch tool can handle this.
+        if (basename($fullPath) eq "ChangeLog") {
+            my $changeLogDotOrigExisted = -f "${fullPath}.orig";
+            my $changeLogHash = fixChangeLogPatch($patch);
+            my $newPatch = setChangeLogDateAndReviewer($changeLogHash->{patch}, $reviewer, $epochTime);
+            applyPatch($newPatch, $fullPath, ["--fuzz=3"]);
+            unlink("${fullPath}.orig") if (! $changeLogDotOrigExisted);
+        } else {
+            applyPatch($patch, $fullPath);
+        }
+    } else {
+        # Either a deletion, an addition or a binary change.
+
+        addDirectoriesIfNeeded(dirname($fullPath));
+
+        if ($isBinary) {
+            if ($isGit) {
+                handleGitBinaryChange($fullPath, $diffHashRef);
+            } else {
+                handleBinaryChange($fullPath, $patch) if $patch;
+            }
+        } elsif ($deletion) {
+            applyPatch($patch, $fullPath, ["--force"]) if $patch;
+            scmRemove($fullPath);
+        } elsif ($addition) {
+            # Addition
+            rename($fullPath, "$fullPath.orig") if -e $fullPath;
+            applyPatch($patch, $fullPath) if $patch;
+            unlink("$fullPath.orig") if -e "$fullPath.orig" && checksum($fullPath) eq checksum("$fullPath.orig");
+            scmAdd($fullPath);
+            my $escapedFullPath = escapeSubversionPath("$fullPath.orig");
+            # What is this for?
+            system("svn", "stat", "$escapedFullPath") if isSVN() && -e "$fullPath.orig";
+        }
+    }
+
+    scmToggleExecutableBit($fullPath, $diffHashRef->{executableBitDelta}) if defined($diffHashRef->{executableBitDelta});
+}
+
+sub removeDirectoriesIfNeeded()
+{
+    foreach my $dir (reverse sort keys %checkedDirectories) {
+        if (isDirectoryEmptyForRemoval($dir)) {
+            scmRemove($dir);
+        }
+    }
+}
+
+# This could be made into a more general "status" call, except svn and git
+# have different ideas about "moving" files which might get confusing.
+sub scmWillDeleteFile($)
+{
+    my ($path) = @_;
+    if (isSVN()) {
+        my $svnOutput = svnStatus($path);
+        return 1 if $svnOutput && substr($svnOutput, 0, 1) eq "D";
+    } elsif (isGit()) {
+        my $command = runCommand("git", "diff-index", "--name-status", "HEAD", "--", $path);
+        return 1 if $command->{stdout} && substr($command->{stdout}, 0, 1) eq "D";
+    }
+    return 0;
+}
+
+# Return whether the file at the given path is known to Git.
+#
+# This method outputs a message like the following to STDERR when
+# returning false:
+#
+# "error: pathspec 'test.png' did not match any file(s) known to git.
+#  Did you forget to 'git add'?"
+sub gitKnowsOfFile($)
+{
+    my $path = shift;
+
+    `git ls-files --error-unmatch -- $path`;
+    my $exitStatus = exitStatus($?);
+    return $exitStatus == 0;
+}
+
+sub scmKnowsOfFile($)
+{
+    my ($path) = @_;
+    if (isSVN()) {
+        my $svnOutput = svnStatus($path);
+        # This will match more than intended.  ? might not be the first field in the status
+        if ($svnOutput && $svnOutput =~ m#\?\s+$path\n#) {
+            return 0;
+        }
+        # This does not handle errors well.
+        return 1;
+    } elsif (isGit()) {
+        my @result = callSilently(\&gitKnowsOfFile, $path);
+        return $result[0];
+    }
+}
+
+sub scmCopy($$)
+{
+    my ($source, $destination) = @_;
+    if (isSVN()) {
+        my $escapedSource = escapeSubversionPath($source);
+        my $escapedDestination = escapeSubversionPath($destination);
+        system("svn", "copy", $escapedSource, $escapedDestination) == 0 or die "Failed to svn copy $escapedSource $escapedDestination.";
+    } elsif (isGit()) {
+        system("cp", $source, $destination) == 0 or die "Failed to copy $source $destination.";
+        system("git", "add", $destination) == 0 or die "Failed to git add $destination.";
+    }
+}
+
+sub scmAdd($)
+{
+    my ($path) = @_;
+    if (isSVN()) {
+        my $escapedPath = escapeSubversionPath($path);
+        system("svn", "add", $escapedPath) == 0 or die "Failed to svn add $escapedPath.";
+    } elsif (isGit()) {
+        system("git", "add", $path) == 0 or die "Failed to git add $path.";
+    }
+}
+
+sub scmRemove($)
+{
+    my ($path) = @_;
+    if (isSVN()) {
+        # SVN is very verbose when removing directories.  Squelch all output except the last line.
+        my $svnOutput;
+        my $escapedPath = escapeSubversionPath($path);
+        open SVN, "svn rm --force '$escapedPath' |" or die "svn rm --force '$escapedPath' failed!";
+        # Only print the last line.  Subversion outputs all changed statuses below $dir
+        while (<SVN>) {
+            $svnOutput = $_;
+        }
+        close SVN;
+        print $svnOutput if $svnOutput;
+    } elsif (isGit()) {
+        # Git removes a directory if it becomes empty when the last file it contains is
+        # removed by `git rm`. In svn-apply this can happen when a directory is being
+        # removed in a patch, and all of the files inside of the directory are removed
+        # before attemping to remove the directory itself. In this case, Git will have 
+        # already deleted the directory and `git rm` would exit with an error claiming
+        # there was no file. The --ignore-unmatch switch gracefully handles this case.
+        system("git", "rm", "--force", "--ignore-unmatch", $path) == 0 or die "Failed to git rm --force --ignore-unmatch $path.";
+    }
+}
diff --git a/Tools/Scripts/svn-create-patch b/Tools/Scripts/svn-create-patch
new file mode 100755
index 0000000..b830f9b
--- /dev/null
+++ b/Tools/Scripts/svn-create-patch
@@ -0,0 +1,437 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005, 2006 Apple Computer, Inc.  All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer. 
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution. 
+# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+#     its contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission. 
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Extended "svn diff" script for WebKit Open Source Project, used to make patches.
+
+# Differences from standard "svn diff":
+#
+#   Uses the real diff, not svn's built-in diff.
+#   Always passes "-p" to diff so it will try to include function names.
+#   Handles binary files (encoded as a base64 chunk of text).
+#   Sorts the diffs alphabetically by text files, then binary files.
+#   Handles copied and moved files.
+#
+# Missing features:
+#
+#   Handle copied and moved directories.
+
+use strict;
+use warnings;
+
+use Config;
+use File::Basename;
+use File::Spec;
+use File::stat;
+use FindBin;
+use Getopt::Long;
+use lib $FindBin::Bin;
+use MIME::Base64;
+use POSIX qw(:errno_h);
+use Time::gmtime;
+use VCSUtils;
+
+sub binarycmp($$);
+sub diffOptionsForFile($);
+sub findBaseUrl($);
+sub findMimeType($;$);
+sub findModificationType($);
+sub findSourceFileAndRevision($);
+sub generateDiff($$);
+sub generateFileList($\%);
+sub hunkHeaderLineRegExForFile($);
+sub isBinaryMimeType($);
+sub manufacturePatchForAdditionWithHistory($);
+sub numericcmp($$);
+sub outputBinaryContent($);
+sub patchpathcmp($$);
+sub pathcmp($$);
+sub processPaths(\@);
+sub splitpath($);
+sub testfilecmp($$);
+
+$ENV{'LC_ALL'} = 'C';
+
+my $showHelp;
+my $ignoreChangelogs = 0;
+my $devNull = File::Spec->devnull();
+
+my $result = GetOptions(
+    "help"       => \$showHelp,
+    "ignore-changelogs"    => \$ignoreChangelogs
+);
+if (!$result || $showHelp) {
+    print STDERR basename($0) . " [-h|--help] [--ignore-changelogs] [svndir1 [svndir2 ...]]\n";
+    exit 1;
+}
+
+# Sort the diffs for easier reviewing.
+my %paths = processPaths(@ARGV);
+
+# Generate a list of files requiring diffs.
+my %diffFiles;
+for my $path (keys %paths) {
+    generateFileList($path, %diffFiles);
+}
+
+my $svnRoot = determineSVNRoot();
+my $prefix = chdirReturningRelativePath($svnRoot);
+
+my $patchSize = 0;
+
+# Generate the diffs, in a order chosen for easy reviewing.
+for my $path (sort patchpathcmp values %diffFiles) {
+    $patchSize += generateDiff($path, $prefix);
+}
+
+if ($patchSize > 20480) {
+    print STDERR "WARNING: Patch's size is " . int($patchSize/1024) . " kbytes.\n";
+    print STDERR "Patches 20k or smaller are more likely to be reviewed. Larger patches may sit unreviewed for a long time.\n";
+}
+
+exit 0;
+
+# Overall sort, considering multiple criteria.
+sub patchpathcmp($$)
+{
+    my ($a, $b) = @_;
+
+    # All binary files come after all non-binary files.
+    my $result = binarycmp($a, $b);
+    return $result if $result;
+
+    # All test files come after all non-test files.
+    $result = testfilecmp($a, $b);
+    return $result if $result;
+
+    # Final sort is a "smart" sort by directory and file name.
+    return pathcmp($a, $b);
+}
+
+# Sort so text files appear before binary files.
+sub binarycmp($$)
+{
+    my ($fileDataA, $fileDataB) = @_;
+    return $fileDataA->{isBinary} <=> $fileDataB->{isBinary};
+}
+
+sub diffOptionsForFile($)
+{
+    my ($file) = @_;
+
+    my $options = "uaNp";
+
+    if (my $hunkHeaderLineRegEx = hunkHeaderLineRegExForFile($file)) {
+        $options .= "F'$hunkHeaderLineRegEx'";
+    }
+
+    return $options;
+}
+
+sub findBaseUrl($)
+{
+    my ($infoPath) = @_;
+    my $baseUrl;
+    my $escapedInfoPath = escapeSubversionPath($infoPath);
+    open INFO, "svn info '$escapedInfoPath' |" or die;
+    while (<INFO>) {
+        if (/^URL: (.+?)[\r\n]*$/) {
+            $baseUrl = $1;
+        }
+    }
+    close INFO;
+    return $baseUrl;
+}
+
+sub findMimeType($;$)
+{
+    my ($file, $revision) = @_;
+    my $args = $revision ? "--revision $revision" : "";
+    my $escapedFile = escapeSubversionPath($file);
+    open PROPGET, "svn propget svn:mime-type $args '$escapedFile' |" or die;
+    my $mimeType = <PROPGET>;
+    close PROPGET;
+    # svn may output a different EOL sequence than $/, so avoid chomp.
+    if ($mimeType) {
+        $mimeType =~ s/[\r\n]+$//g;
+    }
+    return $mimeType;
+}
+
+sub findModificationType($)
+{
+    my ($stat) = @_;
+    my $fileStat = substr($stat, 0, 1);
+    my $propertyStat = substr($stat, 1, 1);
+    if ($fileStat eq "A" || $fileStat eq "R") {
+        my $additionWithHistory = substr($stat, 3, 1);
+        return $additionWithHistory eq "+" ? "additionWithHistory" : "addition";
+    }
+    return "modification" if ($fileStat eq "M" || $propertyStat eq "M");
+    return "deletion" if ($fileStat eq "D");
+    return undef;
+}
+
+sub findSourceFileAndRevision($)
+{
+    my ($file) = @_;
+    my $baseUrl = findBaseUrl(".");
+    my $sourceFile;
+    my $sourceRevision;
+    my $escapedFile = escapeSubversionPath($file);
+    open INFO, "svn info '$escapedFile' |" or die;
+    while (<INFO>) {
+        if (/^Copied From URL: (.+?)[\r\n]*$/) {
+            $sourceFile = File::Spec->abs2rel($1, $baseUrl);
+        } elsif (/^Copied From Rev: ([0-9]+)/) {
+            $sourceRevision = $1;
+        }
+    }
+    close INFO;
+    return ($sourceFile, $sourceRevision);
+}
+
+sub generateDiff($$)
+{
+    my ($fileData, $prefix) = @_;
+    my $file = File::Spec->catdir($prefix, $fileData->{path});
+    
+    if ($ignoreChangelogs && basename($file) eq "ChangeLog") {
+        return 0;
+    }
+    
+    my $patch = "";
+    if ($fileData->{modificationType} eq "additionWithHistory") {
+        manufacturePatchForAdditionWithHistory($fileData);
+    }
+
+    my $diffOptions = diffOptionsForFile($file);
+    my $escapedFile = escapeSubversionPath($file);
+    open DIFF, "svn diff --diff-cmd diff -x -$diffOptions '$escapedFile' |" or die;
+    while (<DIFF>) {
+        $patch .= $_;
+    }
+    close DIFF;
+    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);
+        outputBinaryContent($file);
+    }
+    return length($patch);
+}
+
+sub generateFileList($\%)
+{
+    my ($statPath, $diffFiles) = @_;
+    my %testDirectories = map { $_ => 1 } qw(LayoutTests);
+    my $escapedStatPath = escapeSubversionPath($statPath);
+    open STAT, "svn stat '$escapedStatPath' |" or die;
+    while (my $line = <STAT>) {
+        # svn may output a different EOL sequence than $/, so avoid chomp.
+        $line =~ s/[\r\n]+$//g;
+        my $stat;
+        my $path;
+        if (isSVNVersion16OrNewer()) {
+            $stat = substr($line, 0, 8);
+            $path = substr($line, 8);
+        } else {
+            $stat = substr($line, 0, 7);
+            $path = substr($line, 7);
+        }
+        next if -d $path;
+        my $modificationType = findModificationType($stat);
+        if ($modificationType) {
+            $diffFiles->{$path}->{path} = $path;
+            $diffFiles->{$path}->{modificationType} = $modificationType;
+            $diffFiles->{$path}->{isBinary} = isBinaryMimeType($path);
+            $diffFiles->{$path}->{isTestFile} = exists $testDirectories{(File::Spec->splitdir($path))[0]} ? 1 : 0;
+            if ($modificationType eq "additionWithHistory") {
+                my ($sourceFile, $sourceRevision) = findSourceFileAndRevision($path);
+                $diffFiles->{$path}->{sourceFile} = $sourceFile;
+                $diffFiles->{$path}->{sourceRevision} = $sourceRevision;
+            }
+        } else {
+            print STDERR $line, "\n";
+        }
+    }
+    close STAT;
+}
+
+sub hunkHeaderLineRegExForFile($)
+{
+    my ($file) = @_;
+
+    my $startOfObjCInterfaceRegEx = "@(implementation\\|interface\\|protocol)";
+    return "^[-+]\\|$startOfObjCInterfaceRegEx" if $file =~ /\.mm?$/;
+    return "^$startOfObjCInterfaceRegEx" if $file =~ /^(.*\/)?(mac|objc)\// && $file =~ /\.h$/;
+}
+
+sub isBinaryMimeType($)
+{
+    my ($file) = @_;
+    my $mimeType = findMimeType($file);
+    return 0 if (!$mimeType || substr($mimeType, 0, 5) eq "text/");
+    return 1;
+}
+
+sub manufacturePatchForAdditionWithHistory($)
+{
+    my ($fileData) = @_;
+    my $file = $fileData->{path};
+    print "Index: ${file}\n";
+    print "=" x 67, "\n";
+    my $sourceFile = $fileData->{sourceFile};
+    my $sourceRevision = $fileData->{sourceRevision};
+    print "--- ${file}\t(revision ${sourceRevision})\t(from ${sourceFile}:${sourceRevision})\n";
+    print "+++ ${file}\t(working copy)\n";
+    if ($fileData->{isBinary}) {
+        print "\nCannot display: file marked as a binary type.\n";
+        my $mimeType = findMimeType($file, $sourceRevision);
+        print "svn:mime-type = ${mimeType}\n\n";
+    } else {
+        my $escapedSourceFile = escapeSubversionPath($sourceFile);
+        print `svn cat ${escapedSourceFile} | diff -u $devNull - | tail -n +3`;
+    }
+}
+
+# Sort numeric parts of strings as numbers, other parts as strings.
+# Makes 1.33 come after 1.3, which is cool.
+sub numericcmp($$)
+{
+    my ($aa, $bb) = @_;
+
+    my @a = split /(\d+)/, $aa;
+    my @b = split /(\d+)/, $bb;
+
+    # Compare one chunk at a time.
+    # Each chunk is either all numeric digits, or all not numeric digits.
+    while (@a && @b) {
+        my $a = shift @a;
+        my $b = shift @b;
+        
+        # Use numeric comparison if chunks are non-equal numbers.
+        return $a <=> $b if $a =~ /^\d/ && $b =~ /^\d/ && $a != $b;
+
+        # Use string comparison if chunks are any other kind of non-equal string.
+        return $a cmp $b if $a ne $b;
+    }
+    
+    # One of the two is now empty; compare lengths for result in this case.
+    return @a <=> @b;
+}
+
+sub outputBinaryContent($)
+{
+    my ($path) = @_;
+    # Deletion
+    return if (! -e $path);
+    # Addition or Modification
+    my $buffer;
+    open BINARY, $path  or die;
+    while (read(BINARY, $buffer, 60*57)) {
+        print encode_base64($buffer);
+    }
+    close BINARY;
+    print "\n";
+}
+
+# Sort first by directory, then by file, so all paths in one directory are grouped
+# rather than being interspersed with items from subdirectories.
+# Use numericcmp to sort directory and filenames to make order logical.
+# Also include a special case for ChangeLog, which comes first in any directory.
+sub pathcmp($$)
+{
+    my ($fileDataA, $fileDataB) = @_;
+
+    my ($dira, $namea) = splitpath($fileDataA->{path});
+    my ($dirb, $nameb) = splitpath($fileDataB->{path});
+
+    return numericcmp($dira, $dirb) if $dira ne $dirb;
+    return -1 if $namea eq "ChangeLog" && $nameb ne "ChangeLog";
+    return +1 if $namea ne "ChangeLog" && $nameb eq "ChangeLog";
+    return numericcmp($namea, $nameb);
+}
+
+sub processPaths(\@)
+{
+    my ($paths) = @_;
+    return ("." => 1) if (!@{$paths});
+
+    my %result = ();
+
+    for my $file (@{$paths}) {
+        die "can't handle absolute paths like \"$file\"\n" if File::Spec->file_name_is_absolute($file);
+        die "can't handle empty string path\n" if $file eq "";
+        die "can't handle path with single quote in the name like \"$file\"\n" if $file =~ /'/; # ' (keep Xcode syntax highlighting happy)
+
+        my $untouchedFile = $file;
+
+        $file = canonicalizePath($file);
+
+        die "can't handle paths with .. like \"$untouchedFile\"\n" if $file =~ m|/\.\./|;
+
+        $result{$file} = 1;
+    }
+
+    return ("." => 1) if ($result{"."});
+
+    # Remove any paths that also have a parent listed.
+    for my $path (keys %result) {
+        for (my $parent = dirname($path); $parent ne '.'; $parent = dirname($parent)) {
+            if ($result{$parent}) {
+                delete $result{$path};
+                last;
+            }
+        }
+    }
+
+    return %result;
+}
+
+# Break up a path into the directory (with slash) and base name.
+sub splitpath($)
+{
+    my ($path) = @_;
+
+    my $pathSeparator = "/";
+    my $dirname = dirname($path) . $pathSeparator;
+    $dirname = "" if $dirname eq "." . $pathSeparator;
+
+    return ($dirname, basename($path));
+}
+
+# Sort so source code files appear before test files.
+sub testfilecmp($$)
+{
+    my ($fileDataA, $fileDataB) = @_;
+    return $fileDataA->{isTestFile} <=> $fileDataB->{isTestFile};
+}
+
diff --git a/Tools/Scripts/svn-unapply b/Tools/Scripts/svn-unapply
new file mode 100755
index 0000000..0983617
--- /dev/null
+++ b/Tools/Scripts/svn-unapply
@@ -0,0 +1,283 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005, 2006, 2007 Apple Inc.  All rights reserved.
+# Copyright (C) 2009 Cameron McCormack <cam@mcc.id.au>
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+#
+# 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.
+
+# "unpatch" script for WebKit Open Source Project, used to remove patches.
+
+# Differences from invoking "patch -p0 -R":
+#
+#   Handles added files (does a svn revert with additional logic to handle local changes). 
+#   Handles added directories (does a svn revert and a rmdir).
+#   Handles removed files (does a svn revert with additional logic to handle local changes). 
+#   Handles removed directories (does a svn revert). 
+#   Paths from Index: lines are used rather than the paths on the patch lines, which
+#       makes patches generated by "cvs diff" work (increasingly unimportant since we
+#       use Subversion now).
+#   ChangeLog patches use --fuzz=3 to prevent rejects, and the entry date is reset in
+#       the patch before it is applied (svn-apply sets it when applying a patch).
+#   Handles binary files (requires patches made by svn-create-patch).
+#   Handles copied and moved files (requires patches made by svn-create-patch).
+#   Handles git-diff patches (without binary changes) created at the top-level directory
+#
+# Missing features:
+#
+#   Handle property changes.
+#   Handle copied and moved directories (would require patches made by svn-create-patch).
+#   Use version numbers in the patch file and do a 3-way merge.
+#   When reversing an addition, check that the file matches what's being removed.
+#   Notice a patch that's being unapplied at the "wrong level" and make it work anyway.
+#   Do a dry run on the whole patch and don't do anything if part of the patch is
+#       going to fail (probably too strict unless we exclude ChangeLog).
+#   Handle git-diff patches with binary changes
+
+use strict;
+use warnings;
+
+use Cwd;
+use Digest::MD5;
+use Fcntl qw(:DEFAULT :seek);
+use File::Basename;
+use File::Spec;
+use File::Temp qw(tempfile);
+use Getopt::Long;
+
+use FindBin;
+use lib $FindBin::Bin;
+use VCSUtils;
+
+sub checksum($);
+sub patch($);
+sub revertDirectories();
+sub unapplyPatch($$;$);
+sub unsetChangeLogDate($$);
+
+my $force = 0;
+my $showHelp = 0;
+
+my $optionParseSuccess = GetOptions(
+    "force!" => \$force,
+    "help!" => \$showHelp
+);
+
+if (!$optionParseSuccess || $showHelp) {
+    print STDERR basename($0) . " [-h|--help] [--force] patch1 [patch2 ...]\n";
+    exit 1;
+}
+
+my $globalExitStatus = 0;
+
+my $repositoryRootPath = determineVCSRoot();
+
+my @copiedFiles;
+my %directoriesToCheck;
+
+# Need to use a typeglob to pass the file handle as a parameter,
+# otherwise get a bareword error.
+my @diffHashRefs = parsePatch(*ARGV);
+
+print "Parsed " . @diffHashRefs . " diffs from patch file(s).\n";
+
+my $preparedPatchHash = prepareParsedPatch($force, @diffHashRefs);
+
+my @copyDiffHashRefs = @{$preparedPatchHash->{copyDiffHashRefs}};
+my @nonCopyDiffHashRefs = @{$preparedPatchHash->{nonCopyDiffHashRefs}};
+
+for my $diffHashRef (@nonCopyDiffHashRefs) {
+    patch($diffHashRef);
+}
+
+# Handle copied and moved files last since they may have had post-copy changes that have now been unapplied
+for my $diffHashRef (@copyDiffHashRefs) {
+    patch($diffHashRef);
+}
+
+if (isSVN()) {
+    revertDirectories();
+}
+
+exit $globalExitStatus;
+
+sub checksum($)
+{
+    my $file = shift;
+    open(FILE, $file) or die "Can't open '$file': $!";
+    binmode(FILE);
+    my $checksum = Digest::MD5->new->addfile(*FILE)->hexdigest();
+    close(FILE);
+    return $checksum;
+}
+
+# Args:
+#   $diffHashRef: a diff hash reference of the type returned by parsePatch().
+sub patch($)
+{
+    my ($diffHashRef) = @_;
+
+    # Make sure $patch is initialized to some value.  There is no
+    # svnConvertedText when reversing an svn copy/move.
+    my $patch = $diffHashRef->{svnConvertedText} || "";
+
+    my $fullPath = $diffHashRef->{indexPath};
+    my $isSvnBinary = $diffHashRef->{isBinary} && $diffHashRef->{isSvn};
+    my $hasTextChunks = $patch && $diffHashRef->{numTextChunks};
+
+    $directoriesToCheck{dirname($fullPath)} = 1;
+
+    my $deletion = 0;
+    my $addition = 0;
+
+    $addition = 1 if ($diffHashRef->{isNew} || $diffHashRef->{copiedFromPath} || $patch =~ /\n@@ -0,0 .* @@/);
+    $deletion = 1 if ($diffHashRef->{isDeletion} || $patch =~ /\n@@ .* \+0,0 @@/);
+
+    if (!$addition && !$deletion && !$isSvnBinary && $hasTextChunks) {
+        # Standard patch, patch tool can handle this.
+        if (basename($fullPath) eq "ChangeLog") {
+            my $changeLogDotOrigExisted = -f "${fullPath}.orig";
+            my $changeLogHash = fixChangeLogPatch($patch);
+            unapplyPatch(unsetChangeLogDate($fullPath, $changeLogHash->{patch}), $fullPath, ["--fuzz=3"]);
+            unlink("${fullPath}.orig") if (! $changeLogDotOrigExisted);
+        } else {
+            unapplyPatch($patch, $fullPath);
+        }
+    } else {
+        # Either a deletion, an addition or a binary change.
+
+        my $escapedFullPath = escapeSubversionPath($fullPath);
+        # FIXME: Add support for Git binary files.
+        if ($isSvnBinary) {
+            # Reverse binary change
+            unlink($fullPath) if (-e $fullPath);
+            system "svn", "revert", $escapedFullPath;
+        } elsif ($deletion) {
+            # Reverse deletion
+            rename($fullPath, "$fullPath.orig") if -e $fullPath;
+
+            unapplyPatch($patch, $fullPath);
+
+            # If we don't ask for the filehandle here, we always get a warning.
+            my ($fh, $tempPath) = tempfile(basename($fullPath) . "-XXXXXXXX",
+                                           DIR => dirname($fullPath), UNLINK => 1);
+            close($fh);
+
+            # Keep the version from the patch in case it's different from svn.
+            rename($fullPath, $tempPath);
+            system "svn", "revert", $escapedFullPath;
+            rename($tempPath, $fullPath);
+
+            # This works around a bug in the svn client.
+            # [Issue 1960] file modifications get lost due to FAT 2s time resolution
+            # http://subversion.tigris.org/issues/show_bug.cgi?id=1960
+            system "touch", $fullPath;
+
+            # Remove $fullPath.orig if it is the same as $fullPath
+            unlink("$fullPath.orig") if -e "$fullPath.orig" && checksum($fullPath) eq checksum("$fullPath.orig");
+
+            # Show status if the file is modifed
+            system "svn", "stat", $escapedFullPath;
+        } elsif ($addition) {
+            # Reverse addition
+            #
+            # FIXME: This should use the same logic as svn-apply's deletion
+            #        code.  In particular, svn-apply's scmRemove() subroutine
+            #        should be used here.
+            unapplyPatch($patch, $fullPath, ["--force"]) if $patch;
+            unlink($fullPath) if -z $fullPath;
+            system "svn", "revert", $escapedFullPath;
+        }
+    }
+
+    scmToggleExecutableBit($fullPath, -1 * $diffHashRef->{executableBitDelta}) if defined($diffHashRef->{executableBitDelta});
+}
+
+sub revertDirectories()
+{
+    chdir $repositoryRootPath;
+    my %checkedDirectories;
+    foreach my $path (reverse sort keys %directoriesToCheck) {
+        my @dirs = File::Spec->splitdir($path);
+        while (scalar @dirs) {
+            my $dir = File::Spec->catdir(@dirs);
+            pop(@dirs);
+            next if (exists $checkedDirectories{$dir});
+            if (-d $dir) {
+                my $svnOutput = svnStatus($dir);
+                my $escapedDir = escapeSubversionPath($dir);
+                if ($svnOutput && $svnOutput =~ m#A\s+$dir\n#) {
+                   system "svn", "revert", $escapedDir;
+                   rmdir $dir;
+                }
+                elsif ($svnOutput && $svnOutput =~ m#D\s+$dir\n#) {
+                   system "svn", "revert", $escapedDir;
+                }
+                else {
+                    # Modification
+                    print $svnOutput if $svnOutput;
+                }
+                $checkedDirectories{$dir} = 1;
+            }
+            else {
+                die "'$dir' is not a directory";
+            }
+        }
+    }
+}
+
+# Args:
+#   $patch: a patch string.
+#   $pathRelativeToRoot: the path of the file to be patched, relative to the
+#                        repository root. This should normally be the path
+#                        found in the patch's "Index:" line.
+#   $options: a reference to an array of options to pass to the patch command.
+#             Do not include --reverse in this array.
+sub unapplyPatch($$;$)
+{
+    my ($patch, $pathRelativeToRoot, $options) = @_;
+
+    my $optionalArgs = {options => $options, ensureForce => $force, shouldReverse => 1};
+
+    my $exitStatus = runPatchCommand($patch, $repositoryRootPath, $pathRelativeToRoot, $optionalArgs);
+
+    if ($exitStatus) {
+        $globalExitStatus = $exitStatus;
+    }
+}
+
+sub unsetChangeLogDate($$)
+{
+    my $fullPath = shift;
+    my $patch = shift;
+    my $newDate;
+    sysopen(CHANGELOG, $fullPath, O_RDONLY) or die "Failed to open $fullPath: $!";
+    sysseek(CHANGELOG, 0, SEEK_SET);
+    my $byteCount = sysread(CHANGELOG, $newDate, 10);
+    die "Failed reading $fullPath: $!" if !$byteCount || $byteCount != 10;
+    close(CHANGELOG);
+    $patch =~ s/(\n\+)\d{4}-[^-]{2}-[^-]{2}(  )/$1$newDate$2/;
+    return $patch;
+}
diff --git a/Tools/Scripts/sync-master-with-upstream b/Tools/Scripts/sync-master-with-upstream
new file mode 100755
index 0000000..d2e666c
--- /dev/null
+++ b/Tools/Scripts/sync-master-with-upstream
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+
+# Copyright 2012 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:
+# 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 GOOGLE INC. ``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
+# 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 script is intended to support the GitHub workflow described here:
+# https://trac.webkit.org/wiki/UsingGitHub
+#
+# This script fetches the latest changes from upstream, and pushes those
+# changes to the master branch in origin (e.g., your GitHub fork of WebKit).
+#
+# Running this script periodically will keep your fork of WebKit on GitHub in
+# sync with the "root" WebKit repository in upstream, assuming you've run
+# configure-github-as-upstream
+
+import subprocess
+
+def run(args, error_message = None):
+    if subprocess.call(args) != 0:
+        if error_message:
+            print error_message
+        exit(1)
+
+run(["git", "fetch", "upstream"], "Have you run configure-github-as-upstream to configure an upstream repository?")
+run(["git", "push", "origin", "upstream/master:master"])
+print "\nConsider running 'git merge origin' to update your local branches."
diff --git a/Tools/Scripts/test-webkit-scripts b/Tools/Scripts/test-webkit-scripts
new file mode 100755
index 0000000..781e8ce
--- /dev/null
+++ b/Tools/Scripts/test-webkit-scripts
@@ -0,0 +1,85 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2009, 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+#
+# 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 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 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.
+
+"""Run unit tests of WebKit's Perl and Python scripts."""
+
+# The docstring above is passed as the "description" to the OptionParser
+# used in this script's __main__ block.
+#
+# For the command options supported by this script, see the code below
+# that instantiates the OptionParser class, or else pass --help
+# while running this script (since argument help is auto-generated).
+
+import os
+import subprocess
+import sys
+from optparse import OptionParser
+
+class ScriptsTester(object):
+
+    """Supports running unit tests of WebKit scripts."""
+
+    def __init__(self, scripts_directory):
+        self.scripts_directory = scripts_directory
+
+    def script_path(self, script_file_name):
+        """Return an absolute path to the given script."""
+        return os.path.join(self.scripts_directory, script_file_name)
+
+    def run_test_script(self, script_title, script_path, args=None):
+        """Run the given test script."""
+        print('Testing %s:' % script_title)
+        call_args = [script_path]
+        if args:
+            call_args.extend(args)
+        subprocess.call(call_args)
+        print(70 * "*") # dividing line
+
+    def main(self):
+        parser = OptionParser(description=__doc__)
+        parser.add_option('-a', '--all', dest='all', action='store_true',
+                          default=False, help='run all available tests, '
+                          'including those suppressed by default')
+        (options, args) = parser.parse_args()
+
+        self.run_test_script('Perl scripts', self.script_path('test-webkitperl'))
+        self.run_test_script('Python scripts', self.script_path('test-webkitpy'),
+                             ['--all'] if options.all else None)
+
+        # FIXME: Display a cumulative indication of success or failure.
+        #        In addition, call sys.exit() with 0 or 1 depending on that
+        #        cumulative success or failure.
+        print('Note: Perl and Python results appear separately above.')
+
+
+if __name__ == '__main__':
+    # The scripts directory is the directory containing this file.
+    tester = ScriptsTester(os.path.dirname(__file__))
+    tester.main()
diff --git a/Tools/Scripts/test-webkitperl b/Tools/Scripts/test-webkitperl
new file mode 100755
index 0000000..6faa47c
--- /dev/null
+++ b/Tools/Scripts/test-webkitperl
@@ -0,0 +1,59 @@
+#!/usr/bin/perl
+#
+# Copyright (C) 2009 Chris Jerdonek (chris.jerdonek@gmail.com)
+#
+# 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.
+
+# Runs unit tests of WebKit Perl code.
+
+use strict;
+use warnings;
+
+use File::Spec;
+use FindBin;
+use Test::Harness;
+use lib $FindBin::Bin; # so this script can be run from any directory.
+use VCSUtils;
+
+# Change the working directory so that we can pass shorter, relative
+# paths to runtests(), rather than longer, absolute paths.
+#
+# We change to the source root so the paths can be relative to the
+# source root. These paths display on the screen, and their meaning
+# will be clearer to the user if relative to the root, rather than to
+# the Scripts directory, say.
+#
+# Source root is two levels up from the Scripts directory.
+my $sourceRootDir = File::Spec->catfile($FindBin::Bin, "../..");
+chdir($sourceRootDir);
+
+# Relative to root
+my $pattern = "Tools/Scripts/webkitperl/*_unittest/*.pl";
+
+my @files = <${pattern}>; # lists files alphabetically
+
+runtests(@files);
diff --git a/Tools/Scripts/test-webkitpy b/Tools/Scripts/test-webkitpy
new file mode 100755
index 0000000..ee682cc
--- /dev/null
+++ b/Tools/Scripts/test-webkitpy
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+# Copyright (c) 2011 Google Inc. All rights reserved.
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+from webkitpy.common import multiprocessing_bootstrap
+
+multiprocessing_bootstrap.run('webkitpy', 'test', 'main.py')
diff --git a/Tools/Scripts/update-iexploder-cssproperties b/Tools/Scripts/update-iexploder-cssproperties
new file mode 100755
index 0000000..65e559f
--- /dev/null
+++ b/Tools/Scripts/update-iexploder-cssproperties
@@ -0,0 +1,129 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2007 Apple Inc.  All rights reserved.
+# Copyright (C) 2010 Holger Hans Peter Freyther
+#
+# 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 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.
+
+# This script updates Tools/iExploder/iExploder-1.3.2/htdocs/*.in based on
+# WebCore/css/CSSPropertyNames.in, WebCore/html/HTMLTagNames.in
+# and WebCore/html/HTMLAttributeNames.in
+
+use warnings;
+use strict;
+
+use FindBin;
+use lib $FindBin::Bin;
+use VCSUtils;
+use webkitdirs;
+
+use File::Spec;
+
+sub generateEntityListFromFile($);
+sub readiExploderFile($);
+sub update($$);
+sub writeiExploderFile($@);
+
+update("cssproperties.in", "css/CSSPropertyNames.in");
+update("htmlattrs.in", "html/HTMLAttributeNames.in");
+update("htmltags.in", "html/HTMLTagNames.in");
+print "Successfully updated!\n";
+
+exit 0;
+
+sub generateEntityListFromFile($)
+{
+    my ($filename) = @_;
+
+    my $revision = svnRevisionForDirectory(dirname($filename));
+    my $path = File::Spec->abs2rel($filename, sourceDir());
+    my $result = "# From WebKit svn r" . $revision . " (" . $path . ")\n";
+
+    my @entities = ();
+    my $in_namespace = 0;
+
+    open(IN, $filename) || die "$!";
+    while (my $l = <IN>) {
+        chomp $l;
+        if ($l =~ m/^namespace=\"/) {
+            $in_namespace = 1;
+        } elsif ($in_namespace && $l =~ m/^$/) {
+            $in_namespace = 0;
+        }
+
+        next if $in_namespace;
+        next if $l =~ m/^\s*#/ || $l =~ m/^\s*$/;
+
+        # For HTML Tags that can have additional information
+        if ($l =~ m/ /) {
+            my @split = split / /, $l;
+            $l = $split[0]
+        }
+
+        push(@entities, $l);
+    }
+    close(IN);
+
+    $result .= join("\n", sort { $a cmp $b } @entities) . "\n\n";
+
+    return $result;
+}
+
+sub readiExploderFile($)
+{
+    my ($filename) = @_;
+
+    my @sections = ();
+    local $/ = "\n\n";
+
+    open(IN, $filename) || die "$!";
+    @sections = <IN>;
+    close(IN);
+
+    return @sections;
+}
+
+sub update($$)
+{
+    my ($iexploderPath, $webcorePath) = @_;
+
+    $iexploderPath = File::Spec->catfile(sourceDir(), "Tools", "iExploder", "iExploder-1.3.2", "htdocs", split("/", $iexploderPath));
+    $webcorePath = File::Spec->catfile(sourceDir(), "Source", "WebCore", split("/", $webcorePath));
+
+    my @sections = readiExploderFile($iexploderPath);
+    $sections[0] = generateEntityListFromFile($webcorePath);
+    writeiExploderFile($iexploderPath, @sections);
+}
+
+
+sub writeiExploderFile($@)
+{
+    my ($filename, @sections) = @_;
+
+    open(OUT, "> $filename") || die "$!";
+    print OUT join("", @sections);
+    close(OUT);
+}
+
diff --git a/Tools/Scripts/update-javascriptcore-test-results b/Tools/Scripts/update-javascriptcore-test-results
new file mode 100755
index 0000000..cc8cd2c
--- /dev/null
+++ b/Tools/Scripts/update-javascriptcore-test-results
@@ -0,0 +1,73 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005, 2007 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.
+
+use strict;
+use FindBin;
+use Getopt::Long;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+chdirWebKit();
+chdir "Source/JavaScriptCore/tests/mozilla" or die;
+
+my $force = 0;
+GetOptions('force' => \$force);
+
+open EXPECTED, "expected.html" or die;
+while (<EXPECTED>) {
+    last if /failures reported\.$/;
+}
+my %expected;
+while (<EXPECTED>) {
+    chomp;
+    $expected{$_} = 1;
+}
+close EXPECTED;
+
+open ACTUAL, "actual.html" or die;
+my $actual;
+while (<ACTUAL>) {
+    $actual .= $_;
+    last if /failures reported\.$/;
+}
+my $failed = 0;
+while (<ACTUAL>) {
+    $actual .= $_;
+    chomp;
+    if (!$expected{$_}) {
+        $failed = 1;
+        print "failure not expected: $_\n";
+    }
+}
+close ACTUAL;
+
+die "won't update, failures introduced\n" if $failed && !$force;
+
+open EXPECTED, ">expected.html";
+print EXPECTED $actual;
+close EXPECTED;
diff --git a/Tools/Scripts/update-sources-list.py b/Tools/Scripts/update-sources-list.py
new file mode 100755
index 0000000..2a4a5ef
--- /dev/null
+++ b/Tools/Scripts/update-sources-list.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python
+
+# Copyright (C) 2007 Kevin Ollivier  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 COMPUTER, INC. ``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 COMPUTER, INC. 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.
+#
+# Make sure any port-independent files added to the Bakefile are
+# added to GTK, QT, etc. so that file updates can happen in one place.
+
+import os, sys
+from xml.dom import minidom
+
+scriptDir = os.path.abspath(sys.path[0])
+wkroot = os.path.abspath(os.path.join(scriptDir, "../.."))
+
+def getWebCoreFilesDict():
+    """
+    This method parses the WebCoreSources.bkl file, which has a list of all sources not specific
+    to any port, and returns the result as a dictionary with items of the form
+    (groupName, groupFiles). 
+    """
+    sources = {}
+    sources_prefix = "WEBCORE_"
+    filepath = os.path.join(wkroot, "Source", "WebCore", "WebCoreSources.bkl")
+    assert(os.path.exists(filepath))
+    
+    doc = minidom.parse(filepath)
+    for sourceGroup in doc.getElementsByTagName("set"):
+        groupName = ""
+        if sourceGroup.attributes.has_key("var"):
+            groupName = sourceGroup.attributes["var"].value
+            groupName = groupName.replace(sources_prefix, "")
+            
+        sourcesList = []
+        for node in sourceGroup.childNodes:
+            if node.nodeType == node.TEXT_NODE:
+                sourcesText = node.nodeValue.strip()
+                sourcesList = sourcesText.split("\n")
+                
+        assert(groupName != "")
+        assert(sourcesList != [])
+        
+        sources[groupName] = sourcesList
+        
+    return sources
+
+def generateWebCoreSourcesGTKAndQT(sources):
+    """
+    Convert the dictionary obtained from getWebCoreFilesDict() into a Unix makefile syntax,
+    which IIUC is suitable for both GTK and QT build systems. To take advantage of this,
+    QT and GTK would have to include the file "WebCore/sources.inc" into their makefiles.
+    """
+    makefileString = ""
+    
+    for key in sources.keys():
+        makefileString += key + "+="
+        for source in sources[key]:
+            makefileString += " \\\n\t\t" + source.strip()
+            
+        makefileString += "\n\n"
+    
+    makefileString += "BASE_SOURCES +="
+    for key in sources.keys():
+        makefileString += " \\\n\t\t" + key
+    
+    outfile = os.path.join(wkroot, "Source", "WebCore", "sources.inc")
+    sourcefile = open(outfile, "w")
+    sourcefile.write(makefileString)
+    sourcefile.close()
+    
+sources = getWebCoreFilesDict()
+generateWebCoreSourcesGTKAndQT(sources)
+
+# Coming soon - MSVC and hopefully XCode support!
diff --git a/Tools/Scripts/update-webgl-conformance-tests b/Tools/Scripts/update-webgl-conformance-tests
new file mode 100755
index 0000000..b930836
--- /dev/null
+++ b/Tools/Scripts/update-webgl-conformance-tests
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Wrapper around webkitpy/layout_tests/update-webgl-conformance-tests.py"""
+
+import webkitpy.to_be_moved.update_webgl_conformance_tests
+import sys
+
+if __name__ == '__main__':
+    sys.exit(webkitpy.to_be_moved.update_webgl_conformance_tests.main())
diff --git a/Tools/Scripts/update-webkit b/Tools/Scripts/update-webkit
new file mode 100755
index 0000000..6ba8044
--- /dev/null
+++ b/Tools/Scripts/update-webkit
@@ -0,0 +1,160 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2011 Brent Fulgham. 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.
+
+# Update script for WebKit Open Source Project.
+
+use strict;
+use FindBin;
+use lib $FindBin::Bin;
+use File::Basename;
+use File::Spec;
+use Getopt::Long;
+use VCSUtils;
+use webkitdirs;
+
+sub runSvnUpdate();
+sub runGitUpdate();
+
+# Handle options
+my $quiet = '';
+my $showHelp;
+my $useGYP = 0;
+my $useMake = 0;
+my $useNinja = 0;
+
+determineIsChromium();
+determineIsChromiumAndroid();
+determineIsQt();
+determineIsWinCairo();
+
+chdirWebKit();
+
+my $getOptionsResult = GetOptions(
+    'h|help'  => \$showHelp,
+    'q|quiet' => \$quiet,
+    'gyp' => \$useGYP,
+    'make' => \$useMake,
+    'ninja' => \$useNinja,
+); 
+
+if (!$getOptionsResult || $showHelp) {
+    print STDERR <<__END__;
+Usage: @{[ basename($0) ]} [options]
+  --chromium          also update dependencies of the chromium port
+  --make              generate the Makefile-based build system (Chromium only)
+  --ninja             generate the ninja-based build system (Chromium only)
+  --chromium-android  also update dependencies of the chromium port for Android
+  -h|--help           show the help message
+  -q|--quiet          pass -q to svn update for quiet updates
+  --gyp               generate project files from gyp after update
+  --wincairo          also update dependencies of the WinCairo port
+__END__
+    exit 1;
+}
+
+if ($useMake) {
+    $ENV{"GYP_GENERATORS"} = "make";
+}
+if ($useNinja) {
+    $ENV{"GYP_GENERATORS"} = "ninja";
+}
+
+my @svnOptions = ();
+push @svnOptions, '-q' if $quiet;
+
+# Don't prompt when using svn-1.6 or newer.
+push @svnOptions, qw(--accept postpone) if isSVNVersion16OrNewer();
+
+print "Updating OpenSource\n" unless $quiet;
+runSvnUpdate() if isSVN();
+runGitUpdate() if isGit();
+
+if (-d "../Internal") {
+    chdir("../Internal");
+    print "Updating Internal\n" unless $quiet;
+    runSvnUpdate() if isSVNDirectory(".");
+    runGitUpdate() if isGitDirectory(".");
+} elsif (isChromium()) {
+    my @chromiumUpdateArgs = ("perl", "Tools/Scripts/update-webkit-chromium");
+    push @chromiumUpdateArgs, "--chromium-android" if isChromiumAndroid();
+    push @chromiumUpdateArgs, "--force" if forceChromiumUpdate();
+    system(@chromiumUpdateArgs) == 0 or die $!;
+} elsif (isAppleWinWebKit()) {
+    system("perl", "Tools/Scripts/update-webkit-auxiliary-libs") == 0 or die;
+    if (isWinCairo()) {
+        # WinCairo shares the auxiliary libs from the Apple port.
+        system("perl", "Tools/Scripts/update-webkit-wincairo-libs") == 0 or die;
+    }
+}
+
+setupAppleWinEnv() if isAppleWinWebKit();
+
+if ($useGYP) {
+    print "Generating Project Files\n";
+    system("perl", "Tools/Scripts/generate-project-files") == 0 or die "Failed to run generate-project-files";
+}
+
+exit 0;
+
+sub runSvnUpdate()
+{
+    open UPDATE, "-|", "svn", "update", @svnOptions or die;
+    my @conflictedChangeLogs;
+    while (my $line = <UPDATE>) {
+        print $line;
+        $line =~ m/^C\s+(.+?)[\r\n]*$/;
+        if ($1) {
+          my $filename = normalizePath($1);
+          push @conflictedChangeLogs, $filename if basename($filename) eq "ChangeLog";
+        }
+    }
+    close UPDATE or die;
+
+    if (@conflictedChangeLogs) {
+        print "Attempting to merge conflicted ChangeLogs.\n";
+        my $resolveChangeLogsPath = File::Spec->catfile(dirname($0), "resolve-ChangeLogs");
+        (system($resolveChangeLogsPath, "--no-warnings", @conflictedChangeLogs) == 0)
+            or die "Could not open resolve-ChangeLogs script: $!.\n";
+    }
+}
+
+sub runGitUpdate()
+{
+    # Doing a git fetch first allows setups with svn-remote.svn.fetch = trunk:refs/remotes/origin/master
+    # to perform the rebase much much faster.
+    system("git", "fetch") == 0 or die;
+    if (isGitSVN()) {
+        system("git", "svn", "rebase") == 0 or die;
+    } else {
+        # This will die if branch.$BRANCHNAME.merge isn't set, which is
+        # almost certainly what we want.
+        system("git", "pull") == 0 or die;
+    }
+}
diff --git a/Tools/Scripts/update-webkit-auxiliary-libs b/Tools/Scripts/update-webkit-auxiliary-libs
new file mode 100755
index 0000000..1bb99d5
--- /dev/null
+++ b/Tools/Scripts/update-webkit-auxiliary-libs
@@ -0,0 +1,40 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005, 2006, 2007 Apple Computer, Inc.  All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer. 
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution. 
+# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+#     its contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission. 
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Updates a development environment to the new WebKitAuxiliaryLibrary
+
+use strict;
+use warnings;
+use FindBin;
+
+my $file = "WebKitAuxiliaryLibrary";
+my $zipFile = "$file.zip"; 
+my $auxiliaryLibsURL = "https://developer.apple.com/opensource/internet/$zipFile";
+my $command = "$FindBin::Bin/update-webkit-dependency";
+
+system("perl", $command, $auxiliaryLibsURL, "win") == 0 or die;
diff --git a/Tools/Scripts/update-webkit-chromium b/Tools/Scripts/update-webkit-chromium
new file mode 100755
index 0000000..b21d09e
--- /dev/null
+++ b/Tools/Scripts/update-webkit-chromium
@@ -0,0 +1,96 @@
+#!/usr/bin/perl -w
+# Copyright (C) 2009 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:
+#
+# 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.
+
+# Update script for the WebKit Chromium Port.
+
+use File::Path;
+use FindBin;
+use Getopt::Long;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+determineIsChromiumAndroid();
+
+chdirWebKit();
+chdir("Source/WebKit/chromium") or die $!;
+
+# Find gclient or install it.
+my $gclientPath;
+if (commandExists('gclient')) {
+    $gclientPath = 'gclient';
+} elsif (-e 'depot_tools/gclient') {
+    $gclientPath = 'depot_tools/gclient';
+} else {
+    print "Installing chromium's depot_tools...\n";
+    system("svn co http://src.chromium.org/svn/trunk/tools/depot_tools") == 0 or die $1;
+    $gclientPath = 'depot_tools/gclient';
+}
+
+if (! -e ".gclient") {
+    # If .gclient configuration file doesn't exist, create it.
+    print "Configuring gclient...\n";
+    system($gclientPath,
+           "config",
+           "--spec=solutions=[{'name':'./','url':None}]") == 0 or die $!;
+}
+
+# When building Chromium for Android, the envsetup.sh script needs to be
+# executed prior to project file generation. We need to tell gyp_webkit to do
+# that, as it's a Chromium file and may not be available yet right now.
+if (isChromiumAndroid()) {
+    $ENV{WEBKIT_ANDROID_BUILD} = 1;
+}
+
+my $force = 0;
+GetOptions(
+  'force' => \$force,
+);
+
+# Execute gclient sync.
+print "Updating chromium port dependencies using gclient...\n";
+my @gclientArgs = ($gclientPath, "sync");
+push @gclientArgs, "--force" if $force;
+# --reset could delete modified files if necessary to sync.
+push @gclientArgs, "--reset" if $force;
+push @gclientArgs, "--delete_unversioned_trees" if $force;
+push @gclientArgs, "--deps=unix,android" if isChromiumAndroid();
+
+my $cmd = join(" ",@gclientArgs);
+my $max_attempts = 3;
+my $rc = -1;
+
+# The following will call glient up to $max_attempts times before
+# it gives up and fails.  We need this because glcient can fail
+# for several reasons, some of which are transient (flakiness).
+
+for (1 .. $max_attempts) {
+    $rc = system($cmd);
+    print "Re-trying '" . $cmd . "'\n" if $rc != 0;
+    last if $rc == 0;
+}
+
+die "Error: '$cmd' failed $max_attempts tries and returned " . $rc if ($rc);
diff --git a/Tools/Scripts/update-webkit-dependency b/Tools/Scripts/update-webkit-dependency
new file mode 100755
index 0000000..71d4672
--- /dev/null
+++ b/Tools/Scripts/update-webkit-dependency
@@ -0,0 +1,167 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005, 2006, 2007 Apple Computer, Inc.  All rights reserved.
+# Copyright (C) 2011 Carl Lobo.  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.
+
+# Updates a development environment to the new WebKitAuxiliaryLibrary
+
+use strict;
+use warnings;
+
+use File::Find;
+use File::Spec;
+use File::Temp ();
+use FindBin;
+use HTTP::Date qw(str2time);
+use POSIX;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+if ($#ARGV != 1) {
+    die <<EOF;
+Usage:
+        update-webkit-dependancy <URL with the dependancy zip file> <*prefix dir inside zip without filename>
+
+        * If filename is requirements.zip and the contents of the zipfile are "requirements/x" then prefix = "."
+        * If filename is xyz.zip and the contents of the zipfile are xyz/abc/x" then prefix = "abc"
+        * x is lib or include or bin.
+EOF
+}
+
+sub lastModifiedToUnixTime($);
+sub getLibraryName($);
+
+# Time in seconds that the new zip file must be newer than the old for us to
+# consider them to be different. If the difference in modification time is less
+# than this threshold, we assume that the files are the same. We need this
+# because the zip file is served from a set of mirrors with slightly different
+# Last-Modified times.
+my $newnessThreshold = 30;
+
+my $libsURL = shift;
+my $prefixInZip = shift;
+my $sourceDir = sourceDir();
+my $file = getLibraryName($libsURL);
+my $zipFile = "$file.zip"; 
+my $webkitLibrariesDir = toUnixPath($ENV{'WEBKITLIBRARIESDIR'}) || "$sourceDir/WebKitLibraries/win";
+my $tmpRelativeDir = File::Temp::tempdir("webkitlibsXXXXXXX", TMPDIR => 1, CLEANUP => 1);
+my $tmpAbsDir = File::Spec->rel2abs($tmpRelativeDir);
+
+print "Checking Last-Modified date of $zipFile...\n";
+
+my $result = system "curl -s -I -k --sslv3 $libsURL | grep Last-Modified > \"$tmpAbsDir/$file.headers\"";
+
+if (WEXITSTATUS($result)) {
+
+    #Note: Neither GitHub nor DropBox emit the Last-Modified HTTP header, so fall back to a file
+	#containing the necessary information if we do not receive the information in our initial query.
+    my $headerURL = $libsURL;
+    $headerURL =~ s/\.zip$/\.headers/;
+
+    $result = system "curl -k --sslv3 -o \"$tmpAbsDir/$file.headers\" $headerURL";
+
+    if (WEXITSTATUS($result)) {
+        print STDERR "Couldn't check Last-Modified date of new $zipFile.\n";
+        print STDERR "Please ensure that $libsURL is reachable.\n";
+
+        if (! -f "$webkitLibrariesDir/$file.headers") {
+            print STDERR "Unable to check Last-Modified date and no version of $file to fall back to.\n";
+            exit 1;
+        }
+
+        print STDERR "Falling back to existing version of $file.\n";
+        exit 0;
+    }
+}
+
+if (open NEW, "$tmpAbsDir/$file.headers") {
+    my $new = lastModifiedToUnixTime(<NEW>);
+    close NEW;
+
+    if (defined $new && open OLD, "$webkitLibrariesDir/$file.headers") {
+        my $old = lastModifiedToUnixTime(<OLD>);
+        close OLD;
+        if (defined $old && abs($new - $old) < $newnessThreshold) {
+            print "Current $file is up to date\n";
+            exit 0;
+        }
+    }
+}
+
+print "Downloading $zipFile...\n\n";
+$result = system "curl -k --sslv3 -o \"$tmpAbsDir/$zipFile\" $libsURL";
+die "Couldn't download $zipFile!" if $result;
+
+$result = system "unzip", "-q", "-d", $tmpAbsDir, "$tmpAbsDir/$zipFile";
+die "Couldn't unzip $zipFile." if $result;
+
+print "\nInstalling $file...\n";
+
+sub wanted
+{
+    my $relativeName = File::Spec->abs2rel($File::Find::name, "$tmpAbsDir/$file/$prefixInZip");
+    my $destination = "$webkitLibrariesDir/$relativeName";
+
+    if (-d $_) {
+        mkdir $destination;
+        return;
+    }
+
+    system "cp", $_, $destination;
+}
+
+File::Find::find(\&wanted, "$tmpAbsDir/$file");
+
+$result = system "mv", "$tmpAbsDir/$file.headers", $webkitLibrariesDir;
+print STDERR "Couldn't move $file.headers to $webkitLibrariesDir" . ".\n" if $result;
+
+print "The $file has been sucessfully installed in\n $webkitLibrariesDir\n";
+exit;
+
+sub toUnixPath
+{
+    my $path = shift;
+    return unless $path;
+    chomp($path = `cygpath -u '$path'`);
+    return $path;
+}
+
+sub lastModifiedToUnixTime($)
+{
+    my ($str) = @_;
+
+    $str =~ /^Last-Modified: (.*)$/ or return;
+    return str2time($1);
+}
+
+sub getLibraryName($)
+{
+    my $url = shift;
+    $url =~ m#/([^/]+)\.zip$#;
+    return $1;
+}
+
diff --git a/Tools/Scripts/update-webkit-libs-jhbuild b/Tools/Scripts/update-webkit-libs-jhbuild
new file mode 100755
index 0000000..51605bd
--- /dev/null
+++ b/Tools/Scripts/update-webkit-libs-jhbuild
@@ -0,0 +1,128 @@
+#!/usr/bin/perl -w
+# Copyright (C) 2011 Igalia S.L.
+# Copyright (C) 2012 Intel Corporation
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+use Getopt::Long;
+
+my $platformEfl = 0;
+my $platformGtk = 0;
+
+my $getOptionsResult = GetOptions(
+    'efl' => \$platformEfl,
+    'gtk' => \$platformGtk
+    );
+
+my $platform = "";
+if (!$getOptionsResult) {
+    die "No platform specified for " . basename($0) .". Use --gtk or --efl.\n";
+} else {
+    if ($platformEfl) {
+        $platform = "efl";
+    }
+    if ($platformGtk) {
+        $platform = "gtk";
+    }
+}
+
+sub getMD5HashForFile($)
+{
+    my $file = shift;
+
+    open(FILE_CONTENTS, $file);
+
+    my $contents = "";
+    while (<FILE_CONTENTS>) {
+        $contents .= $_;
+    }
+
+    close(FILE_CONTENTS);
+
+    return md5_hex($contents);
+}
+
+sub jhbuildConfigurationChanged()
+{
+    foreach my $file (qw(jhbuildrc jhbuild.modules)) {
+        my $path = join('/', getJhbuildPath(), $file . '.md5sum');
+        if (! -e $path) {
+            return 1;
+        }
+
+        # Get the md5 sum of the file we're testing, look in the right platform directory.
+        my $actualFile = join('/', sourceDir(), 'Tools', $platform, $file);
+        my $currentSum = getMD5HashForFile($actualFile);
+
+        # Get our previous record.
+        open(PREVIOUS_MD5, $path);
+        chomp(my $previousSum = <PREVIOUS_MD5>);
+        close(PREVIOUS_MD5);
+
+        if ($previousSum ne $currentSum) {
+            return 1;
+        }
+    }
+}
+
+sub saveJhbuildMd5() {
+    # Save md5sum for jhbuild-related files.saveJhbuildMd5();
+    foreach my $file (qw(jhbuildrc jhbuild.modules)) {
+        my $source = join('/', sourceDir(), "Tools", $platform, $file);
+        my $destination = join('/', getJhbuildPath(), $file);
+        open(SUM, ">$destination" . ".md5sum");
+        print SUM getMD5HashForFile($source);
+        close(SUM);
+    }
+}
+
+sub runJhbuild
+{
+    my $command = shift;
+    my @jhbuildArgs = ("./jhbuild-wrapper", "--".$platform, $command);
+    push(@jhbuildArgs, @ARGV[2..-1]);
+    system(@jhbuildArgs) == 0 or die "Running jhbuild-wrapper " . $command . " failed.\n";
+}
+
+sub cleanJhbuild()
+{
+    runJhbuild("clean");
+
+    # If the configuration changed, dependencies may have been removed.
+    # Since we lack a granular way of uninstalling those we wipe out the
+    # jhbuild root and start from scratch.
+    my $jhbuildPath = getJhbuildPath();
+    if (system("rm -rf $jhbuildPath/Root") ne 0) {
+        die "Cleaning jhbuild root failed!";
+    }
+}
+
+delete $ENV{AR_FLAGS} if exists $ENV{AR_FLAGS};
+
+chdir(relativeScriptsDir() . "/../jhbuild") or die $!;
+
+my %prettyPlatform = ( "efl" => "EFL", "gtk" => "GTK+" );
+
+if (-e getJhbuildPath() && jhbuildConfigurationChanged()) {
+    cleanJhbuild();
+}
+
+print "Updating " . $prettyPlatform{$platform} . " port dependencies using jhbuild...\n";
+runJhbuild("build");
+
+saveJhbuildMd5();
diff --git a/Tools/Scripts/update-webkit-localizable-strings b/Tools/Scripts/update-webkit-localizable-strings
new file mode 100755
index 0000000..ceb25a5
--- /dev/null
+++ b/Tools/Scripts/update-webkit-localizable-strings
@@ -0,0 +1,45 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2006, 2007 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.
+
+use strict;
+use warnings;
+
+use File::Basename;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+# WebKit and WebKit2 strings go into WebCore's Localizable.strings.
+my @directoriesToScan = ("Source/WebCore", "Source/WebKit/mac", "Source/WebKit/win", "Source/WebKit2", "-Source/WebCore/icu", "-Source/WebKit/mac/icu");
+my $fileToUpdate = "Source/WebCore/English.lproj/Localizable.strings";
+
+@ARGV == 0 or die "Usage: " . basename($0) . "\n";
+
+chdirWebKit();
+
+system "Tools/Scripts/extract-localizable-strings", "-", $fileToUpdate, @directoriesToScan;
diff --git a/Tools/Scripts/update-webkit-support-libs b/Tools/Scripts/update-webkit-support-libs
new file mode 100755
index 0000000..afced1d
--- /dev/null
+++ b/Tools/Scripts/update-webkit-support-libs
@@ -0,0 +1,149 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2005, 2006, 2007 Apple Computer, Inc.  All rights reserved.
+# 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
+# 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.
+
+# Updates a development environment to the new WebKitSupportLibrary
+
+use strict;
+use warnings;
+
+use File::Find;
+use File::Temp ();
+use File::Spec;
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+use constant NOTAVERSION => "-1";
+
+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 $tmpRelativeDir = File::Temp::tempdir("webkitlibsXXXXXXX", TMPDIR => 1, CLEANUP => 1);
+my $tmpAbsDir = File::Spec->rel2abs($tmpRelativeDir);
+my $versionFileURL = "https://developer.apple.com/opensource/internet/$versionFile";
+
+my $extractedVersion = extractedVersion();
+
+# Check whether the extracted library is up-to-date. If it is, we don't have anything to do.
+my $expectedVersion = downloadExpectedVersionNumber();
+if ($extractedVersion ne NOTAVERSION && $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.
+my $zipFileVersion = zipFileVersion();
+dieAndInstructToDownload("$zipFile could not be found in $zipDirectory.") if $zipFileVersion eq NOTAVERSION;
+dieAndInstructToDownload("$zipFile is out-of-date.") if $expectedVersion ne NOTAVERSION && $zipFileVersion ne $expectedVersion;
+if ($zipFileVersion eq $extractedVersion) {
+    print "Falling back to existing version of $file.\n";
+    exit;
+}
+
+my $result = system "unzip", "-q", "-d", $tmpAbsDir, $pathToZip;
+die "Couldn't unzip $zipFile." if $result;
+
+print "\nInstalling $file...\n";
+
+sub wanted
+{
+    my $relativeName = File::Spec->abs2rel($File::Find::name, "$tmpAbsDir/$file/win");
+    my $destination = "$webkitLibrariesDir/$relativeName";
+
+    if (-d $_) {
+        mkdir $destination;
+        return;
+    }
+
+    system "cp", $_, $destination;
+}
+
+File::Find::find(\&wanted, "$tmpAbsDir/$file");
+
+print "The $file has been sucessfully installed in\n $webkitLibrariesDir\n";
+exit;
+
+sub toUnixPath
+{
+    my $path = shift;
+    return unless $path;
+    chomp($path = `cygpath -u '$path'`);
+    return $path;
+}
+
+sub extractedVersion
+{
+    if (open VERSION, "<", $pathToVersionFile) {
+        chomp(my $extractedVersion = <VERSION>);
+        close VERSION;
+        return $extractedVersion;
+    }
+    return NOTAVERSION;
+}
+
+sub downloadExpectedVersionNumber
+{
+    chomp(my $expectedVersion = `curl -s --sslv3 -k $versionFileURL`);
+    return WEXITSTATUS($?) ? NOTAVERSION : $expectedVersion;
+}
+
+sub zipFileVersion
+{
+    return NOTAVERSION unless -f $pathToZip;
+    chomp(my $zipFileVersion = `unzip -p "$pathToZip" $file/win/$versionFile`);
+    return $zipFileVersion;
+}
+
+sub dieAndInstructToDownload
+{
+    my $message = shift;
+
+    die <<EOF;
+
+===============================================================================
+$message
+Please download $zipFile from:
+
+    https://developer.apple.com/opensource/internet/webkit_sptlib_agree.html
+
+and place it in:
+
+    $sourceDir
+
+Then run build-webkit again.
+===============================================================================
+
+EOF
+
+}
diff --git a/Tools/Scripts/update-webkit-wincairo-libs b/Tools/Scripts/update-webkit-wincairo-libs
new file mode 100755
index 0000000..52d052e
--- /dev/null
+++ b/Tools/Scripts/update-webkit-wincairo-libs
@@ -0,0 +1,41 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2012 Brent Fulgham <bfulgham@webkit.org>.  All rights reserved.
+# Copyright (C) 2011 Carl Lobo.  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.
+
+# Updates a development environment to the new WebKitAuxiliaryLibrary
+
+use strict;
+use warnings;
+use FindBin;
+
+my $file = "WinCairoRequirements";
+my $zipFile = "$file.zip";
+my $winCairoLibsURL = "http://dl.dropbox.com/u/39598926/$zipFile";
+my $command = "$FindBin::Bin/update-webkit-dependency";
+
+system("perl", $command, $winCairoLibsURL, ".") == 0 or die;
diff --git a/Tools/Scripts/update-webkitefl-libs b/Tools/Scripts/update-webkitefl-libs
new file mode 100755
index 0000000..30b2104
--- /dev/null
+++ b/Tools/Scripts/update-webkitefl-libs
@@ -0,0 +1,23 @@
+#!/usr/bin/perl -w
+# Copyright (C) 2012 Intel Corporation
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+my $scriptsDir = relativeScriptsDir();
+system("perl", "$scriptsDir/update-webkit-libs-jhbuild", "--efl") == 0 or die $!;
diff --git a/Tools/Scripts/update-webkitgtk-libs b/Tools/Scripts/update-webkitgtk-libs
new file mode 100755
index 0000000..792cc28
--- /dev/null
+++ b/Tools/Scripts/update-webkitgtk-libs
@@ -0,0 +1,24 @@
+#!/usr/bin/perl -w
+# Copyright (C) 2011 Igalia S.L.
+# Copyright (C) 2012 Intel Corporation
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+my $scriptsDir = relativeScriptsDir();
+system("perl", "$scriptsDir/update-webkit-libs-jhbuild", "--gtk", @ARGV) == 0 or die $!;
diff --git a/Tools/Scripts/validate-committer-lists b/Tools/Scripts/validate-committer-lists
new file mode 100755
index 0000000..96763d4
--- /dev/null
+++ b/Tools/Scripts/validate-committer-lists
@@ -0,0 +1,287 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2009, 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.
+#
+# Checks Python's known list of committers against lists.webkit.org and SVN history.
+
+
+import os
+import subprocess
+import re
+import urllib2
+from datetime import date, datetime, timedelta
+from optparse import OptionParser
+
+from webkitpy.common.config.committers import CommitterList
+from webkitpy.common.system.deprecated_logging import log, error
+from webkitpy.common.checkout.scm import Git
+from webkitpy.common.net.bugzilla import Bugzilla
+
+# WebKit includes a built copy of BeautifulSoup in Scripts/webkitpy
+# so this import should always succeed.
+from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup
+
+
+def print_list_if_non_empty(title, list_to_print):
+    if not list_to_print:
+        return
+    print # Newline before the list
+    print title
+    for item in list_to_print:
+        print item
+
+
+class CommitterListFromMailingList(object):
+    committers_list_url = "http://lists.webkit.org/mailman/roster.cgi/webkit-committers"
+    reviewers_list_url = "http://lists.webkit.org/mailman/roster.cgi/webkit-reviewers"
+
+    def _fetch_emails_from_page(self, url):
+        page = urllib2.urlopen(url)
+        soup = BeautifulSoup(page)
+
+        emails = []
+        # Grab the cells in the first column (which happens to be the bug ids).
+        for email_item in soup('li'):
+            email_link = email_item.find("a")
+            email = email_link.string.replace(" at ", "@") # The email is obfuscated using " at " instead of "@".
+            emails.append(email)
+        return emails
+
+    @staticmethod
+    def _commiters_not_found_in_email_list(committers, emails):
+        missing_from_mailing_list = []
+        for committer in committers:
+            for email in committer.emails:
+                if email in emails:
+                    break
+            else:
+                missing_from_mailing_list.append(committer)
+        return missing_from_mailing_list
+
+    @staticmethod
+    def _emails_not_found_in_committer_list(committers, emails):
+        email_to_committer_map = {}
+        for committer in committers:
+            for email in committer.emails:
+                email_to_committer_map[email] = committer
+
+        return filter(lambda email: not email_to_committer_map.get(email), emails)
+
+    def check_for_emails_missing_from_list(self, committer_list):
+        committer_emails = self._fetch_emails_from_page(self.committers_list_url)
+        list_name = "webkit-committers@lists.webkit.org"
+
+        missing_from_mailing_list = self._commiters_not_found_in_email_list(committer_list.committers(), committer_emails)
+        print_list_if_non_empty("Committers missing from %s:" % list_name, missing_from_mailing_list)
+
+        users_missing_from_committers = self._emails_not_found_in_committer_list(committer_list.committers(), committer_emails)
+        print_list_if_non_empty("Subcribers to %s missing from committer.py:" % list_name, users_missing_from_committers)
+
+
+        reviewer_emails = self._fetch_emails_from_page(self.reviewers_list_url)
+        list_name = "webkit-reviewers@lists.webkit.org"
+
+        missing_from_mailing_list = self._commiters_not_found_in_email_list(committer_list.reviewers(), reviewer_emails)
+        print_list_if_non_empty("Reviewers missing from %s:" % list_name, missing_from_mailing_list)
+
+        missing_from_reviewers = self._emails_not_found_in_committer_list(committer_list.reviewers(), reviewer_emails)
+        print_list_if_non_empty("Subcribers to %s missing from reviewers in committer.py:" % list_name, missing_from_reviewers)
+
+        missing_from_committers = self._emails_not_found_in_committer_list(committer_list.committers(), reviewer_emails)
+        print_list_if_non_empty("Subcribers to %s completely missing from committers.py" % list_name, missing_from_committers)
+
+
+class CommitterListFromGit(object):
+    login_to_email_address = {
+        'aliceli1' : 'alice.liu@apple.com',
+        'bdash' : 'mrowe@apple.com',
+        'bdibello' : 'bdibello@apple.com', # Bruce DiBello, only 4 commits: r10023, r9548, r9538, r9535
+        'cblu' : 'cblu@apple.com',
+        'cpeterse' : 'cpetersen@apple.com',
+        'eseidel' : 'eric@webkit.org',
+        'gdennis' : 'gdennis@webkit.org',
+        'goldsmit' : 'goldsmit@apple.com', # Debbie Goldsmith, only one commit r8839
+        'gramps' : 'gramps@apple.com',
+        'honeycutt' : 'jhoneycutt@apple.com',
+        'jdevalk' : 'joost@webkit.org',
+        'jens' : 'jens@apple.com',
+        'justing' : 'justin.garcia@apple.com',
+        'kali' : 'kali@apple.com', # Christy Warren, did BIDI work, 5 commits: r8815, r8802, r8801, r8791, r8773, r8603
+        'kjk' : 'kkowalczyk@gmail.com',
+        'kmccullo' : 'kmccullough@apple.com',
+        'kocienda' : 'kocienda@apple.com',
+        'lamadio' : 'lamadio@apple.com', # Lou Amadio, only 2 commits: r17949 and r17783
+        'lars' : 'lars@kde.org',
+        'lweintraub' : 'lweintraub@apple.com',
+        'lypanov' : 'lypanov@kde.org',
+        'mhay' : 'mhay@apple.com', # Mike Hay, 3 commits: r3813, r2552, r2548
+        'ouch' : 'ouch@apple.com', # John Louch
+        'pyeh' : 'patti@apple.com', # Patti Yeh, did VoiceOver work in WebKit
+        'rjw' : 'rjw@apple.com',
+        'seangies' : 'seangies@apple.com', # Sean Gies?, only 5 commits: r16600, r16592, r16511, r16489, r16484
+        'sheridan' : 'sheridan@apple.com', # Shelly Sheridan
+        'thatcher' : 'timothy@apple.com',
+        'tomernic' : 'timo@apple.com',
+        'trey' : 'trey@usa.net',
+        'tristan' : 'tristan@apple.com',
+        'vicki' : 'vicki@apple.com',
+        'voas' : 'voas@apple.com', # Ed Voas, did some Carbon work in WebKit
+        'zack' : 'zack@kde.org',
+        'zimmermann' : 'zimmermann@webkit.org',
+    }
+
+    def __init__(self):
+        self._last_commit_time_by_author_cache = {}
+
+    def _fetch_authors_and_last_commit_time_from_git_log(self):
+        last_commit_dates = {}
+        git_log_args = ['git', 'log', '--reverse', '--pretty=format:%ae %at']
+        process = subprocess.Popen(git_log_args, stdout=subprocess.PIPE)
+
+        # eric@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc 1257090899
+        line_regexp = re.compile("^(?P<author>.+)@\S+ (?P<timestamp>\d+)$")
+        while True:
+            output_line = process.stdout.readline()
+            if output_line == '' and process.poll() != None:
+                return last_commit_dates
+
+            match_result = line_regexp.match(output_line)
+            if not match_result:
+                error("Failed to match line: %s" % output_line)
+            last_commit_dates[match_result.group('author')] = float(match_result.group('timestamp'))
+
+    def _fill_in_emails_for_old_logins(self):
+        authors_missing_email = filter(lambda author: author.find('@') == -1, self._last_commit_time_by_author_cache)
+        authors_with_email = filter(lambda author: author.find('@') != -1, self._last_commit_time_by_author_cache)
+        prefixes_of_authors_with_email = map(lambda author: author.split('@')[0], authors_with_email)
+
+        for author in authors_missing_email:
+            # First check to see if we have a manual mapping from login to email.
+            author_email = self.login_to_email_address.get(author)
+
+            # Most old logins like 'darin' are now just 'darin@apple.com', so check for a prefix match if a manual mapping was not found.
+            if not author_email and author in prefixes_of_authors_with_email:
+                author_email_index = prefixes_of_authors_with_email.index(author)
+                author_email = authors_with_email[author_email_index]
+
+            if not author_email:
+                # No known email mapping, likely not an active committer.  We could log here.
+                continue
+
+            # log("%s -> %s" % (author, author_email)) # For sanity checking.
+            no_email_commit_time = self._last_commit_time_by_author_cache.get(author)
+            email_commit_time = self._last_commit_time_by_author_cache.get(author_email)
+            # We compare the timestamps for extra sanity even though we could assume commits before email address were used for login are always going to be older.
+            if not email_commit_time or email_commit_time < no_email_commit_time:
+                self._last_commit_time_by_author_cache[author_email] = no_email_commit_time
+            del self._last_commit_time_by_author_cache[author]
+
+    def _last_commit_by_author(self):
+        if not self._last_commit_time_by_author_cache:
+            self._last_commit_time_by_author_cache = self._fetch_authors_and_last_commit_time_from_git_log()
+            self._fill_in_emails_for_old_logins()
+            del self._last_commit_time_by_author_cache['(no author)'] # The initial svn import isn't very useful.
+        return self._last_commit_time_by_author_cache
+
+    @staticmethod
+    def _print_three_column_row(widths, values):
+        print "%s%s%s" % (values[0].ljust(widths[0]), values[1].ljust(widths[1]), values[2])
+
+    def print_possibly_expired_committers(self, committer_list):
+        authors_and_last_commits = self._last_commit_by_author().items()
+        authors_and_last_commits.sort(lambda a,b: cmp(a[1], b[1]), reverse=True)
+        committer_cuttof = date.today() - timedelta(days=365)
+        column_widths = [13, 25]
+        print
+        print "Committers who have not committed within one year:"
+        self._print_three_column_row(column_widths, ("Last Commit", "Committer Email", "Committer Record"))
+        for (author, last_commit) in authors_and_last_commits:
+            last_commit_date = date.fromtimestamp(last_commit)
+            if committer_cuttof > last_commit_date:
+                committer_record = committer_list.committer_by_email(author)
+                self._print_three_column_row(column_widths, (str(last_commit_date), author, committer_record))
+
+    def print_committers_missing_from_committer_list(self, committer_list):
+        missing_from_committers_py = []
+        last_commit_time_by_author = self._last_commit_by_author()
+        for author in last_commit_time_by_author:
+            if not committer_list.committer_by_email(author):
+                missing_from_committers_py.append(author)
+
+        never_committed = []
+        for committer in committer_list.committers():
+            for email in committer.emails:
+                if last_commit_time_by_author.get(email):
+                    break
+            else:
+                never_committed.append(committer)
+
+        print_list_if_non_empty("Historical committers missing from committer.py:", missing_from_committers_py)
+        print_list_if_non_empty("Committers in committer.py who have never committed:", never_committed)
+
+
+class CommitterListBugzillaChecker(object):
+    def __init__(self):
+        self._bugzilla = Bugzilla()
+
+    def _has_invalid_bugzilla_email(self, committer):
+        return not self._bugzilla.queries.fetch_logins_matching_substring(committer.bugzilla_email())
+
+    def print_committers_with_invalid_bugzilla_emails(self, committer_list):
+        print # Print a newline before we start hitting bugzilla (it logs about logging in).
+        print "Checking committer emails against bugzilla (this will take a long time)"
+        committers_with_invalid_bugzilla_email = filter(self._has_invalid_bugzilla_email, committer_list.committers())
+        print_list_if_non_empty("Committers with invalid bugzilla email:", committers_with_invalid_bugzilla_email)
+
+
+def main():
+    parser = OptionParser()
+    parser.add_option("-b", "--check-bugzilla-emails", action="store_true", help="Check the bugzilla_email for each committer against bugs.webkit.org")
+    (options, args) = parser.parse_args()
+
+    committer_list = CommitterList()
+    CommitterListFromMailingList().check_for_emails_missing_from_list(committer_list)
+ 
+    if not Git.in_working_directory("."):
+        print """\n\nWARNING: validate-committer-lists requires a git checkout.
+The following checks are disabled:
+ - List of committers ordered by last commit
+ - List of historical committers missing from committers.py
+"""
+        return 1
+    svn_committer_list = CommitterListFromGit()
+    svn_committer_list.print_possibly_expired_committers(committer_list)
+    svn_committer_list.print_committers_missing_from_committer_list(committer_list)
+
+    if options.check_bugzilla_emails:
+        CommitterListBugzillaChecker().print_committers_with_invalid_bugzilla_emails(committer_list)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/Tools/Scripts/webkit-build-directory b/Tools/Scripts/webkit-build-directory
new file mode 100755
index 0000000..d5085b5
--- /dev/null
+++ b/Tools/Scripts/webkit-build-directory
@@ -0,0 +1,83 @@
+#!/usr/bin/perl
+
+# 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:
+#
+# 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.
+
+# A script to expose WebKit's build directory detection logic to non-perl scripts.
+
+use FindBin;
+use Getopt::Long;
+
+use lib $FindBin::Bin;
+use webkitdirs;
+
+my $showConfigurationDirectory = 0;
+my $showHelp = 0;
+my $showTopLevelDirectory = 0;
+
+
+my $programName = basename($0);
+my $usage = <<EOF;
+Usage: $programName [options]
+  --configuration       Show the build directory for a specific configuration (e.g. Debug, Release.  Defaults to the active configuration set by set-webkit-configuration)
+  -h|--help             Show this help message
+  --top-level           Show the top-level build directory
+
+  --blackberry          Find the build directory for the BlackBerry port on Mac/Linux
+  --chromium            Find the build directory for the Chromium port on Mac/Win/Linux
+  --chromium-android    Find the build directory for the Chromium port on Android
+  --efl                 Find the build directory for the EFL port
+  --gtk                 Find the build directory for the GTK+ port
+  --qt                  Find the build directory for the Qt port
+  --wincairo            Find the build directory for using Cairo (rather than CoreGraphics) on Windows
+  --wince               Find the build directory for the WinCE port
+
+Either --configuration or --top-level is required.
+EOF
+
+setConfiguration(); # Figure out from the command line if we're --debug or --release or the default.
+
+# FIXME: Check if extra flags are valid or not.
+Getopt::Long::Configure('pass_through'); # Let --blackberry, etc... be handled by webkitdirs
+my $getOptionsResult = GetOptions(
+    'configuration' => \$showConfigurationDirectory,
+    'top-level' => \$showTopLevelDirectory,
+    'help|h' => \$showHelp,
+);
+
+if (!$getOptionsResult || $showHelp) {
+    print STDERR $usage;
+    exit 1;
+}
+
+if (!$showConfigurationDirectory && !$showTopLevelDirectory) {
+    print baseProductDir() . "\n";
+    print productDir() . "\n";
+} elsif ($showTopLevelDirectory) {
+    print baseProductDir() . "\n";
+} else {
+    print productDir() . "\n";
+}
diff --git a/Tools/Scripts/webkit-patch b/Tools/Scripts/webkit-patch
new file mode 100755
index 0000000..2ed9d8d
--- /dev/null
+++ b/Tools/Scripts/webkit-patch
@@ -0,0 +1,86 @@
+#!/usr/bin/env python
+# Copyright (c) 2011 Code Aurora Forum. All rights reserved.
+# Copyright (c) 2010 Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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 tool for automating dealing with bugzilla, posting patches, committing patches, etc.
+
+import logging
+import os
+import signal
+import sys
+import codecs
+
+import webkitpy.common.version_check
+
+from webkitpy.common.system.logutils import configure_logging
+from webkitpy.tool.main import WebKitPatch
+
+# A StreamWriter will by default try to encode all objects passed
+# to write(), so when passed a raw string already encoded as utf8,
+# it will blow up with an UnicodeDecodeError. This does not match
+# the default behaviour of writing to sys.stdout, so we intercept
+# the case of writing raw strings and make sure StreamWriter gets
+# input that it can handle.
+class ForgivingUTF8Writer(codecs.lookup('utf-8')[-1]):
+    def write(self, object):
+        if isinstance(object, str):
+            # Assume raw strings are utf-8 encoded. If this line
+            # fails with an UnicodeDecodeError, our assumption was
+            # wrong, and the stacktrace should show you where we
+            # write non-Unicode/UTF-8 data (which we shouldn't).
+            object = object.decode('utf-8')
+        return codecs.StreamWriter.write(self, object)
+
+# By default, sys.stdout assumes ascii encoding.  Since our messages can
+# contain unicode strings (as with some peoples' names) we need to apply
+# the utf-8 codec to prevent throwing and exception.
+# Not having this was the cause of https://bugs.webkit.org/show_bug.cgi?id=63452.
+sys.stdout = ForgivingUTF8Writer(sys.stdout)
+
+_log = logging.getLogger("webkit-patch")
+
+def main():
+    # This is a hack to let us enable DEBUG logging as early as possible.
+    # Note this can't be ternary as versioning.check_version()
+    # hasn't run yet and this python might be older than 2.5.
+    if set(["-v", "--verbose"]).intersection(set(sys.argv)):
+        logging_level = logging.DEBUG
+    else:
+        logging_level = logging.INFO
+    configure_logging(logging_level=logging_level)
+    WebKitPatch(os.path.abspath(__file__)).main()
+
+
+if __name__ == "__main__":
+    try:
+        main()
+    except KeyboardInterrupt:
+        sys.exit(signal.SIGINT + 128)
diff --git a/Tools/Scripts/webkit-tools-completion.sh b/Tools/Scripts/webkit-tools-completion.sh
new file mode 100755
index 0000000..d83a5a6
--- /dev/null
+++ b/Tools/Scripts/webkit-tools-completion.sh
@@ -0,0 +1,114 @@
+# Copyright (C) 2009 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.
+#
+# Command line completion for common commands used in WebKit development.
+#
+# Set-up:
+#   Add a line like this to your .bashrc:
+#     source /path/to/WebKitCode/Tools/Scripts/webkit-tools-completion.sh
+
+__webkit-patch_generate_reply()
+{
+    COMPREPLY=( $(compgen -W "$1" -- "${COMP_WORDS[COMP_CWORD]}") )
+}
+
+__webkit-patch_upload_cc_generate_reply()
+{
+    # Note: This won't work well if hostname completion is enabled, disable it with: shopt -u hostcomplete
+    # Completion is done on tokens and our comma-separated list is one single token, so we have to do completion on the whole list each time.
+    # Return a \n separated list for each possible bugzilla email completion of the substring following the last comma.
+    # Redirect strerr to /dev/null to prevent noise in the shell if this ever breaks somehow.
+    COMPREPLY=( $(PYTHONPATH=$(dirname "${BASH_SOURCE[0]}") python -c "
+import sys,re
+from webkitpy.common.config.committers import CommitterList
+m = re.match('((.*,)*)(.*)', sys.argv[1])
+untilLastComma = m.group(1)
+afterLastComma = m.group(3)
+print('\n'.join([untilLastComma + c.bugzilla_email() + ',' for c in CommitterList().contributors() if c.bugzilla_email().startswith(afterLastComma)]))" "${COMP_WORDS[COMP_CWORD]}" 2>/dev/null ) )
+}
+
+_webkit-patch_complete()
+{
+    local command current_command="${COMP_WORDS[1]}"
+    case "$current_command" in
+        -h|--help)
+            command="help";
+            ;;
+        *)
+            command="$current_command"
+            ;;
+    esac
+
+    if [ $COMP_CWORD -eq 1 ]; then
+        __webkit-patch_generate_reply "--help apply-from-bug bugs-to-commit commit-message land land-from-bug obsolete-attachments patches-to-commit post upload tree-status rollout reviewed-patches"
+        return
+    fi
+
+    case "$command" in
+        apply-from-bug)
+            __webkit-patch_generate_reply "--force-clean --local-commit --no-clean --no-update"
+            return
+            ;;
+        commit-message)
+            return
+            ;;
+        land)
+            __webkit-patch_generate_reply "--no-build --no-close --no-test --reviewer= -r"
+            return
+            ;;
+        land-from-bug)
+            __webkit-patch_generate_reply "--force-clean --no-build --no-clean --no-test"
+            return
+            ;;
+        obsolete-attachments)
+            return
+            ;;
+        post)
+            __webkit-patch_generate_reply "--description --no-obsolete --no-review --request-commit -m --open-bug"
+            return
+            ;;
+        upload)
+            if [[ ${COMP_WORDS[COMP_CWORD-1]} == "--cc" || ${COMP_WORDS[COMP_CWORD-1]} == "=" && ${COMP_WORDS[COMP_CWORD-2]} == "--cc" ]]; then
+                __webkit-patch_upload_cc_generate_reply
+                return
+            fi
+            __webkit-patch_generate_reply "--description --no-obsolete --no-review --request-commit --cc -m --open-bug"
+            return
+            ;;
+        post-commits)
+            __webkit-patch_generate_reply "--bug-id= --no-comment --no-obsolete --no-review -b"
+            return
+            ;;
+    esac
+}
+
+complete -F _webkit-patch_complete webkit-patch
+complete -o default -W "--continue --fix-merged --help --no-continue --no-warnings --warnings -c -f -h -w" resolve-ChangeLogs
+complete -o default -W "--bug --diff --git-commit --git-index --git-reviewer --help --no-update --no-write --open --update --write -d -h -o" prepare-ChangeLog
+complete -W "--clean --debug --help -h" build-webkit
+complete -o default -W "--add-platform-exceptions --complex-text --configuration --guard-malloc --help --http --ignore-tests --launch-safari --leaks --merge-leak-depth --new-test-results --no-http --no-launch-safari --no-new-test-results --no-sample-on-timeout --no-strip-editing-callbacks --pixel-tests --platform --port --quiet --random --reset-results --results-directory --reverse --root --sample-on-timeout --singly --skipped --slowest --strict --strip-editing-callbacks --threaded --timeout --tolerance --use-remote-links-to-tests --valgrind --verbose -1 -c -g -h -i -l -m -o -p -q -t -v" run-webkit-tests
diff --git a/Tools/Scripts/webkitdirs.pm b/Tools/Scripts/webkitdirs.pm
new file mode 100755
index 0000000..e8c6cab
--- /dev/null
+++ b/Tools/Scripts/webkitdirs.pm
@@ -0,0 +1,2761 @@
+# Copyright (C) 2005, 2006, 2007, 2010, 2011, 2012 Apple Inc. All rights reserved.
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2011 Research In Motion Limited. 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.
+
+# Module to share code to get to WebKit directories.
+
+use strict;
+use version;
+use warnings;
+use Config;
+use Digest::MD5 qw(md5_hex);
+use FindBin;
+use File::Basename;
+use File::Path qw(mkpath rmtree);
+use File::Spec;
+use File::stat;
+use POSIX;
+use VCSUtils;
+
+BEGIN {
+   use Exporter   ();
+   our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
+   $VERSION     = 1.00;
+   @ISA         = qw(Exporter);
+   @EXPORT      = qw(
+       &XcodeOptionString
+       &XcodeOptionStringNoConfig
+       &XcodeOptions
+       &baseProductDir
+       &chdirWebKit
+       &checkFrameworks
+       &cmakeBasedPortArguments
+       &cmakeBasedPortName
+       &currentSVNRevision
+       &debugSafari
+       &nmPath
+       &passedConfiguration
+       &printHelpAndExitForRunAndDebugWebKitAppIfNeeded
+       &productDir
+       &runMacWebKitApp
+       &safariPath
+       &setConfiguration
+       USE_OPEN_COMMAND
+   );
+   %EXPORT_TAGS = ( );
+   @EXPORT_OK   = ();
+}
+
+use constant USE_OPEN_COMMAND => 1; # Used in runMacWebKitApp().
+use constant INCLUDE_OPTIONS_FOR_DEBUGGING => 1;
+
+our @EXPORT_OK;
+
+my $architecture;
+my $numberOfCPUs;
+my $baseProductDir;
+my @baseProductDirOption;
+my $configuration;
+my $configurationForVisualStudio;
+my $configurationProductDir;
+my $sourceDir;
+my $currentSVNRevision;
+my $debugger;
+my $nmPath;
+my $osXVersion;
+my $generateDsym;
+my $isQt;
+my $qmakebin = "qmake"; # Allow override of the qmake binary from $PATH
+my $isGtk;
+my $isWinCE;
+my $isWinCairo;
+my $isWx;
+my $isEfl;
+my @wxArgs;
+my $isBlackBerry;
+my $isChromium;
+my $isChromiumAndroid;
+my $isChromiumMacMake;
+my $isChromiumNinja;
+my $forceChromiumUpdate;
+my $isInspectorFrontend;
+my $isWK2;
+my $shouldTargetWebProcess;
+my $shouldUseXPCServiceForWebProcess;
+my $shouldUseGuardMalloc;
+my $xcodeVersion;
+
+# Variables for Win32 support
+my $vcBuildPath;
+my $windowsSourceDir;
+my $winVersion;
+my $willUseVCExpressWhenBuilding = 0;
+
+# Defined in VCSUtils.
+sub exitStatus($);
+
+sub determineSourceDir
+{
+    return if $sourceDir;
+    $sourceDir = $FindBin::Bin;
+    $sourceDir =~ s|/+$||; # Remove trailing '/' as we would die later
+
+    # walks up path checking each directory to see if it is the main WebKit project dir, 
+    # defined by containing Sources, WebCore, and WebKit
+    until ((-d "$sourceDir/Source" && -d "$sourceDir/Source/WebCore" && -d "$sourceDir/Source/WebKit") || (-d "$sourceDir/Internal" && -d "$sourceDir/OpenSource"))
+    {
+        if ($sourceDir !~ s|/[^/]+$||) {
+            die "Could not find top level webkit directory above source directory using FindBin.\n";
+        }
+    }
+
+    $sourceDir = "$sourceDir/OpenSource" if -d "$sourceDir/OpenSource";
+}
+
+sub currentPerlPath()
+{
+    my $thisPerl = $^X;
+    if ($^O ne 'VMS') {
+        $thisPerl .= $Config{_exe} unless $thisPerl =~ m/$Config{_exe}$/i;
+    }
+    return $thisPerl;
+}
+
+sub setQmakeBinaryPath($)
+{
+    ($qmakebin) = @_;
+}
+
+# used for scripts which are stored in a non-standard location
+sub setSourceDir($)
+{
+    ($sourceDir) = @_;
+}
+
+sub determineXcodeVersion
+{
+    return if defined $xcodeVersion;
+    my $xcodebuildVersionOutput = `xcodebuild -version`;
+    $xcodeVersion = ($xcodebuildVersionOutput =~ /Xcode ([0-9](\.[0-9]+)*)/) ? $1 : "3.0";
+}
+
+sub readXcodeUserDefault($)
+{
+    my ($unprefixedKey) = @_;
+
+    determineXcodeVersion();
+
+    my $xcodeDefaultsDomain = (eval "v$xcodeVersion" lt v4) ? "com.apple.Xcode" : "com.apple.dt.Xcode";
+    my $xcodeDefaultsPrefix = (eval "v$xcodeVersion" lt v4) ? "PBX" : "IDE";
+    my $devnull = File::Spec->devnull();
+
+    my $value = `defaults read $xcodeDefaultsDomain ${xcodeDefaultsPrefix}${unprefixedKey} 2> ${devnull}`;
+    return if $?;
+
+    chomp $value;
+    return $value;
+}
+
+sub determineBaseProductDir
+{
+    return if defined $baseProductDir;
+    determineSourceDir();
+
+    my $setSharedPrecompsDir;
+    $baseProductDir = $ENV{"WEBKITOUTPUTDIR"};
+
+    if (!defined($baseProductDir) and isAppleMacWebKit()) {
+        # Silently remove ~/Library/Preferences/xcodebuild.plist which can
+        # cause build failure. The presence of
+        # ~/Library/Preferences/xcodebuild.plist can prevent xcodebuild from
+        # respecting global settings such as a custom build products directory
+        # (<rdar://problem/5585899>).
+        my $personalPlistFile = $ENV{HOME} . "/Library/Preferences/xcodebuild.plist";
+        if (-e $personalPlistFile) {
+            unlink($personalPlistFile) || die "Could not delete $personalPlistFile: $!";
+        }
+
+        determineXcodeVersion();
+
+        if (eval "v$xcodeVersion" ge v4) {
+            my $buildLocationStyle = join '', readXcodeUserDefault("BuildLocationStyle");
+            if ($buildLocationStyle eq "Custom") {
+                my $buildLocationType = join '', readXcodeUserDefault("CustomBuildLocationType");
+                # FIXME: Read CustomBuildIntermediatesPath and set OBJROOT accordingly.
+                $baseProductDir = readXcodeUserDefault("CustomBuildProductsPath") if $buildLocationType eq "Absolute";
+            }
+
+            # DeterminedByTargets corresponds to a setting of "Legacy" in Xcode.
+            # It is the only build location style for which SHARED_PRECOMPS_DIR is not
+            # overridden when building from within Xcode.
+            $setSharedPrecompsDir = 1 if $buildLocationStyle ne "DeterminedByTargets";
+        }
+
+        if (!defined($baseProductDir)) {
+            $baseProductDir = join '', readXcodeUserDefault("ApplicationwideBuildSettings");
+            $baseProductDir = $1 if $baseProductDir =~ /SYMROOT\s*=\s*\"(.*?)\";/s;
+        }
+
+        undef $baseProductDir unless $baseProductDir =~ /^\//;
+    } elsif (isChromium()) {
+        if (isLinux() || isChromiumAndroid() || isChromiumMacMake()) {
+            $baseProductDir = "$sourceDir/out";
+        } elsif (isDarwin()) {
+            $baseProductDir = "$sourceDir/Source/WebKit/chromium/xcodebuild";
+        } elsif (isWindows() || isCygwin()) {
+            $baseProductDir = "$sourceDir/Source/WebKit/chromium/build";
+        }
+    }
+
+    if (!defined($baseProductDir)) { # Port-specific checks failed, use default
+        $baseProductDir = "$sourceDir/WebKitBuild";
+    }
+
+    if (isBlackBerry()) {
+        my %archInfo = blackberryTargetArchitecture();
+        $baseProductDir = "$baseProductDir/" . $archInfo{"cpuDir"};
+    }
+
+    if (isGit() && isGitBranchBuild() && !isChromium()) {
+        my $branch = gitBranch();
+        $baseProductDir = "$baseProductDir/$branch";
+    }
+
+    if (isAppleMacWebKit()) {
+        $baseProductDir =~ s|^\Q$(SRCROOT)/..\E$|$sourceDir|;
+        $baseProductDir =~ s|^\Q$(SRCROOT)/../|$sourceDir/|;
+        $baseProductDir =~ s|^~/|$ENV{HOME}/|;
+        die "Can't handle Xcode product directory with a ~ in it.\n" if $baseProductDir =~ /~/;
+        die "Can't handle Xcode product directory with a variable in it.\n" if $baseProductDir =~ /\$/;
+        @baseProductDirOption = ("SYMROOT=$baseProductDir", "OBJROOT=$baseProductDir");
+        push(@baseProductDirOption, "SHARED_PRECOMPS_DIR=${baseProductDir}/PrecompiledHeaders") if $setSharedPrecompsDir;
+    }
+
+    if (isCygwin()) {
+        my $dosBuildPath = `cygpath --windows \"$baseProductDir\"`;
+        chomp $dosBuildPath;
+        $ENV{"WEBKITOUTPUTDIR"} = $dosBuildPath;
+        my $unixBuildPath = `cygpath --unix \"$baseProductDir\"`;
+        chomp $unixBuildPath;
+        $baseProductDir = $unixBuildPath;
+    }
+}
+
+sub setBaseProductDir($)
+{
+    ($baseProductDir) = @_;
+}
+
+sub determineConfiguration
+{
+    return if defined $configuration;
+    determineBaseProductDir();
+    if (open CONFIGURATION, "$baseProductDir/Configuration") {
+        $configuration = <CONFIGURATION>;
+        close CONFIGURATION;
+    }
+    if ($configuration) {
+        chomp $configuration;
+        # compatibility for people who have old Configuration files
+        $configuration = "Release" if $configuration eq "Deployment";
+        $configuration = "Debug" if $configuration eq "Development";
+    } else {
+        $configuration = "Release";
+    }
+
+    if ($configuration && isWinCairo()) {
+        unless ($configuration =~ /_Cairo_CFLite$/) {
+            $configuration .= "_Cairo_CFLite";
+        }
+    }
+}
+
+sub determineArchitecture
+{
+    return if defined $architecture;
+    # make sure $architecture is defined in all cases
+    $architecture = "";
+
+    determineBaseProductDir();
+
+    if (isGtk()) {
+        determineConfigurationProductDir();
+        my $host_triple = `grep -E '^host = ' $configurationProductDir/GNUmakefile`;
+        if ($host_triple =~ m/^host = ([^-]+)-/) {
+            # We have a configured build tree; use it.
+            $architecture = $1;
+        }
+    } elsif (isAppleMacWebKit()) {
+        if (open ARCHITECTURE, "$baseProductDir/Architecture") {
+            $architecture = <ARCHITECTURE>;
+            close ARCHITECTURE;
+        }
+        if ($architecture) {
+            chomp $architecture;
+        } else {
+            my $supports64Bit = `sysctl -n hw.optional.x86_64`;
+            chomp $supports64Bit;
+            $architecture = 'x86_64' if $supports64Bit;
+        }
+    } elsif (isEfl()) {
+        my $host_processor = "";
+        $host_processor = `cmake --system-information | grep CMAKE_SYSTEM_PROCESSOR`;
+        if ($host_processor =~ m/^CMAKE_SYSTEM_PROCESSOR \"([^"]+)\"/) {
+            # We have a configured build tree; use it.
+            $architecture = $1;
+            $architecture = 'x86_64' if $architecture eq 'amd64';
+        }
+    }
+
+    if (!$architecture && (isGtk() || isAppleMacWebKit() || isEfl())) {
+        # Fall back to output of `arch', if it is present.
+        $architecture = `arch`;
+        chomp $architecture;
+    }
+
+    if (!$architecture && (isGtk() || isAppleMacWebKit() || isEfl())) {
+        # Fall back to output of `uname -m', if it is present.
+        $architecture = `uname -m`;
+        chomp $architecture;
+    }
+}
+
+sub determineNumberOfCPUs
+{
+    return if defined $numberOfCPUs;
+    if (defined($ENV{NUMBER_OF_PROCESSORS})) {
+        $numberOfCPUs = $ENV{NUMBER_OF_PROCESSORS};
+    } elsif (isLinux()) {
+        # First try the nproc utility, if it exists. If we get no
+        # results fall back to just interpretting /proc directly.
+        chomp($numberOfCPUs = `nproc --all 2> /dev/null`);
+        if ($numberOfCPUs eq "") {
+            $numberOfCPUs = (grep /processor/, `cat /proc/cpuinfo`);
+        }
+    } elsif (isWindows() || isCygwin()) {
+        # Assumes cygwin
+        $numberOfCPUs = `ls /proc/registry/HKEY_LOCAL_MACHINE/HARDWARE/DESCRIPTION/System/CentralProcessor | wc -w`;
+    } elsif (isDarwin() || isFreeBSD()) {
+        chomp($numberOfCPUs = `sysctl -n hw.ncpu`);
+    }
+}
+
+sub jscPath($)
+{
+    my ($productDir) = @_;
+    my $jscName = "jsc";
+    $jscName .= "_debug"  if configurationForVisualStudio() eq "Debug_All";
+    $jscName .= ".exe" if (isWindows() || isCygwin());
+    return "$productDir/$jscName" if -e "$productDir/$jscName";
+    return "$productDir/JavaScriptCore.framework/Resources/$jscName";
+}
+
+sub argumentsForConfiguration()
+{
+    determineConfiguration();
+    determineArchitecture();
+
+    my @args = ();
+    push(@args, '--debug') if $configuration eq "Debug";
+    push(@args, '--release') if $configuration eq "Release";
+    push(@args, '--32-bit') if $architecture ne "x86_64";
+    push(@args, '--qt') if isQt();
+    push(@args, '--gtk') if isGtk();
+    push(@args, '--efl') if isEfl();
+    push(@args, '--wincairo') if isWinCairo();
+    push(@args, '--wince') if isWinCE();
+    push(@args, '--wx') if isWx();
+    push(@args, '--blackberry') if isBlackBerry();
+    push(@args, '--chromium') if isChromium() && !isChromiumAndroid();
+    push(@args, '--chromium-android') if isChromiumAndroid();
+    push(@args, '--inspector-frontend') if isInspectorFrontend();
+    return @args;
+}
+
+sub determineConfigurationForVisualStudio
+{
+    return if defined $configurationForVisualStudio;
+    determineConfiguration();
+    # FIXME: We should detect when Debug_All or Production has been chosen.
+    $configurationForVisualStudio = $configuration;
+}
+
+sub usesPerConfigurationBuildDirectory
+{
+    # [Gtk] We don't have Release/Debug configurations in straight
+    # autotool builds (non build-webkit). In this case and if
+    # WEBKITOUTPUTDIR exist, use that as our configuration dir. This will
+    # allows us to run run-webkit-tests without using build-webkit.
+    return ($ENV{"WEBKITOUTPUTDIR"} && isGtk()) || isAppleWinWebKit();
+}
+
+sub determineConfigurationProductDir
+{
+    return if defined $configurationProductDir;
+    determineBaseProductDir();
+    determineConfiguration();
+    if (isAppleWinWebKit() && !isWx()) {
+        $configurationProductDir = File::Spec->catdir($baseProductDir, configurationForVisualStudio(), "bin");
+    } else {
+        if (usesPerConfigurationBuildDirectory()) {
+            $configurationProductDir = "$baseProductDir";
+        } else {
+            $configurationProductDir = "$baseProductDir/$configuration";
+        }
+    }
+}
+
+sub setConfigurationProductDir($)
+{
+    ($configurationProductDir) = @_;
+}
+
+sub determineCurrentSVNRevision
+{
+    # We always update the current SVN revision here, and leave the caching
+    # to currentSVNRevision(), so that changes to the SVN revision while the
+    # script is running can be picked up by calling this function again.
+    determineSourceDir();
+    $currentSVNRevision = svnRevisionForDirectory($sourceDir);
+    return $currentSVNRevision;
+}
+
+
+sub chdirWebKit
+{
+    determineSourceDir();
+    chdir $sourceDir or die;
+}
+
+sub baseProductDir
+{
+    determineBaseProductDir();
+    return $baseProductDir;
+}
+
+sub sourceDir
+{
+    determineSourceDir();
+    return $sourceDir;
+}
+
+sub productDir
+{
+    determineConfigurationProductDir();
+    return $configurationProductDir;
+}
+
+sub jscProductDir
+{
+    my $productDir = productDir();
+    $productDir .= "/bin" if (isQt() || isEfl());
+    $productDir .= "/Programs" if isGtk();
+
+    return $productDir;
+}
+
+sub configuration()
+{
+    determineConfiguration();
+    return $configuration;
+}
+
+sub configurationForVisualStudio()
+{
+    determineConfigurationForVisualStudio();
+    return $configurationForVisualStudio;
+}
+
+sub currentSVNRevision
+{
+    determineCurrentSVNRevision() if not defined $currentSVNRevision;
+    return $currentSVNRevision;
+}
+
+sub generateDsym()
+{
+    determineGenerateDsym();
+    return $generateDsym;
+}
+
+sub determineGenerateDsym()
+{
+    return if defined($generateDsym);
+    $generateDsym = checkForArgumentAndRemoveFromARGV("--dsym");
+}
+
+sub argumentsForXcode()
+{
+    my @args = ();
+    push @args, "DEBUG_INFORMATION_FORMAT=dwarf-with-dsym" if generateDsym();
+    return @args;
+}
+
+sub XcodeOptions
+{
+    determineBaseProductDir();
+    determineConfiguration();
+    determineArchitecture();
+    return (@baseProductDirOption, "-configuration", $configuration, "ARCHS=$architecture", argumentsForXcode());
+}
+
+sub XcodeOptionString
+{
+    return join " ", XcodeOptions();
+}
+
+sub XcodeOptionStringNoConfig
+{
+    return join " ", @baseProductDirOption;
+}
+
+sub XcodeCoverageSupportOptions()
+{
+    my @coverageSupportOptions = ();
+    push @coverageSupportOptions, "GCC_GENERATE_TEST_COVERAGE_FILES=YES";
+    push @coverageSupportOptions, "GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES";
+    push @coverageSupportOptions, "EXTRA_LINK= \$(EXTRA_LINK) -ftest-coverage -fprofile-arcs";
+    push @coverageSupportOptions, "OTHER_CFLAGS= \$(OTHER_CFLAGS) -DCOVERAGE -MD";
+    push @coverageSupportOptions, "OTHER_LDFLAGS=\$(OTHER_LDFLAGS) -ftest-coverage -fprofile-arcs -lgcov";
+    return @coverageSupportOptions;
+}
+
+my $passedConfiguration;
+my $searchedForPassedConfiguration;
+sub determinePassedConfiguration
+{
+    return if $searchedForPassedConfiguration;
+    $searchedForPassedConfiguration = 1;
+
+    for my $i (0 .. $#ARGV) {
+        my $opt = $ARGV[$i];
+        if ($opt =~ /^--debug$/i || $opt =~ /^--devel/i) {
+            splice(@ARGV, $i, 1);
+            $passedConfiguration = "Debug";
+            $passedConfiguration .= "_Cairo_CFLite" if (isWinCairo() && isCygwin());
+            return;
+        }
+        if ($opt =~ /^--release$/i || $opt =~ /^--deploy/i) {
+            splice(@ARGV, $i, 1);
+            $passedConfiguration = "Release";
+            $passedConfiguration .= "_Cairo_CFLite" if (isWinCairo() && isCygwin());
+            return;
+        }
+        if ($opt =~ /^--profil(e|ing)$/i) {
+            splice(@ARGV, $i, 1);
+            $passedConfiguration = "Profiling";
+            $passedConfiguration .= "_Cairo_CFLite" if (isWinCairo() && isCygwin());
+            return;
+        }
+    }
+    $passedConfiguration = undef;
+}
+
+sub passedConfiguration
+{
+    determinePassedConfiguration();
+    return $passedConfiguration;
+}
+
+sub setConfiguration
+{
+    setArchitecture();
+
+    if (my $config = shift @_) {
+        $configuration = $config;
+        return;
+    }
+
+    determinePassedConfiguration();
+    $configuration = $passedConfiguration if $passedConfiguration;
+}
+
+
+my $passedArchitecture;
+my $searchedForPassedArchitecture;
+sub determinePassedArchitecture
+{
+    return if $searchedForPassedArchitecture;
+    $searchedForPassedArchitecture = 1;
+
+    for my $i (0 .. $#ARGV) {
+        my $opt = $ARGV[$i];
+        if ($opt =~ /^--32-bit$/i) {
+            splice(@ARGV, $i, 1);
+            if (isAppleMacWebKit() || isWx()) {
+                $passedArchitecture = `arch`;
+                chomp $passedArchitecture;
+            }
+            return;
+        }
+    }
+    $passedArchitecture = undef;
+}
+
+sub passedArchitecture
+{
+    determinePassedArchitecture();
+    return $passedArchitecture;
+}
+
+sub architecture()
+{
+    determineArchitecture();
+    return $architecture;
+}
+
+sub numberOfCPUs()
+{
+    determineNumberOfCPUs();
+    return $numberOfCPUs;
+}
+
+sub setArchitecture
+{
+    if (my $arch = shift @_) {
+        $architecture = $arch;
+        return;
+    }
+
+    determinePassedArchitecture();
+    $architecture = $passedArchitecture if $passedArchitecture;
+}
+
+sub executableHasEntitlements
+{
+    my $executablePath = shift;
+    return (`codesign -d --entitlements - $executablePath 2>&1` =~ /<key>/);
+}
+
+sub safariPathFromSafariBundle
+{
+    my ($safariBundle) = @_;
+
+    if (isAppleMacWebKit()) {
+        my $safariPath = "$safariBundle/Contents/MacOS/Safari";
+        my $safariForWebKitDevelopmentPath = "$safariBundle/Contents/MacOS/SafariForWebKitDevelopment";
+        return $safariForWebKitDevelopmentPath if -f $safariForWebKitDevelopmentPath && executableHasEntitlements($safariPath);
+        return $safariPath;
+    }
+    return $safariBundle if isAppleWinWebKit();
+}
+
+sub installedSafariPath
+{
+    my $safariBundle;
+
+    if (isAppleMacWebKit()) {
+        $safariBundle = "/Applications/Safari.app";
+    } elsif (isAppleWinWebKit()) {
+        $safariBundle = readRegistryString("/HKLM/SOFTWARE/Apple Computer, Inc./Safari/InstallDir");
+        $safariBundle =~ s/[\r\n]+$//;
+        $safariBundle = `cygpath -u '$safariBundle'` if isCygwin();
+        $safariBundle =~ s/[\r\n]+$//;
+        $safariBundle .= "Safari.exe";
+    }
+
+    return safariPathFromSafariBundle($safariBundle);
+}
+
+# Locate Safari.
+sub safariPath
+{
+    # Use WEBKIT_SAFARI environment variable if present.
+    my $safariBundle = $ENV{WEBKIT_SAFARI};
+    if (!$safariBundle) {
+        determineConfigurationProductDir();
+        # Use Safari.app in product directory if present (good for Safari development team).
+        if (isAppleMacWebKit() && -d "$configurationProductDir/Safari.app") {
+            $safariBundle = "$configurationProductDir/Safari.app";
+        } elsif (isAppleWinWebKit()) {
+            my $path = "$configurationProductDir/Safari.exe";
+            my $debugPath = "$configurationProductDir/Safari_debug.exe";
+
+            if (configurationForVisualStudio() eq "Debug_All" && -x $debugPath) {
+                $safariBundle = $debugPath;
+            } elsif (-x $path) {
+                $safariBundle = $path;
+            }
+        }
+        if (!$safariBundle) {
+            return installedSafariPath();
+        }
+    }
+    my $safariPath = safariPathFromSafariBundle($safariBundle);
+    die "Can't find executable at $safariPath.\n" if isAppleMacWebKit() && !-x $safariPath;
+    return $safariPath;
+}
+
+sub builtDylibPathForName
+{
+    my $libraryName = shift;
+    determineConfigurationProductDir();
+    if (isChromium()) {
+        return "$configurationProductDir/$libraryName";
+    }
+    if (isBlackBerry()) {
+        my $libraryExtension = $libraryName =~ /^WebKit$/i ? ".so" : ".a";
+        return "$configurationProductDir/$libraryName/lib" . lc($libraryName) . $libraryExtension;
+    }
+    if (isQt()) {
+        my $isSearchingForWebCore = $libraryName =~ "WebCore";
+        $libraryName = "QtWebKitWidgets";
+        my $result;
+        if (isDarwin() and -d "$configurationProductDir/lib/$libraryName.framework") {
+            $result = "$configurationProductDir/lib/$libraryName.framework/$libraryName";
+        } elsif (isDarwin() and -d "$configurationProductDir/lib") {
+            $result = "$configurationProductDir/lib/lib$libraryName.dylib";
+        } elsif (isWindows()) {
+            if (configuration() eq "Debug") {
+                # On Windows, there is a "d" suffix to the library name. See <http://trac.webkit.org/changeset/53924/>.
+                $libraryName .= "d";
+            }
+
+            chomp(my $mkspec = `$qmakebin -query QT_HOST_DATA`);
+            $mkspec .= "/mkspecs";
+            my $qtMajorVersion = retrieveQMakespecVar("$mkspec/qconfig.pri", "QT_MAJOR_VERSION");
+            if (not $qtMajorVersion) {
+                $qtMajorVersion = "";
+            }
+
+            $result = "$configurationProductDir/lib/$libraryName$qtMajorVersion.dll";
+        } else {
+            $result = "$configurationProductDir/lib/lib$libraryName.so";
+        }
+
+        if ($isSearchingForWebCore) {
+            # With CONFIG+=force_static_libs_as_shared we have a shared library for each subdir.
+            # For feature detection to work it is necessary to return the path of the WebCore library here.
+            my $replacedWithWebCore = $result;
+            $replacedWithWebCore =~ s/$libraryName/WebCore/g;
+            if (-e $replacedWithWebCore) {
+                return $replacedWithWebCore;
+            }
+        }
+
+        return $result;
+    }
+    if (isWx()) {
+        return "$configurationProductDir/libwxwebkit.dylib";
+    }
+    if (isGtk()) {
+        # WebKitGTK+ for GTK2, WebKitGTK+ for GTK3, and WebKit2 respectively.
+        my @libraries = ("libwebkitgtk-1.0", "libwebkitgtk-3.0", "libwebkit2gtk-1.0");
+        my $extension = isDarwin() ? ".dylib" : ".so";
+
+        foreach $libraryName (@libraries) {
+            my $libraryPath = "$configurationProductDir/.libs/" . $libraryName . $extension;
+            return $libraryPath if -e $libraryPath;
+        }
+        return "NotFound";
+    }
+    if (isEfl()) {
+        return "$configurationProductDir/lib/libewebkit.so";
+    }
+    if (isWinCE()) {
+        return "$configurationProductDir/$libraryName";
+    }
+    if (isAppleMacWebKit()) {
+        return "$configurationProductDir/$libraryName.framework/Versions/A/$libraryName";
+    }
+    if (isAppleWinWebKit()) {
+        if ($libraryName eq "JavaScriptCore") {
+            return "$baseProductDir/lib/$libraryName.lib";
+        } else {
+            return "$baseProductDir/$libraryName.intermediate/$configuration/$libraryName.intermediate/$libraryName.lib";
+        }
+    }
+
+    die "Unsupported platform, can't determine built library locations.\nTry `build-webkit --help` for more information.\n";
+}
+
+# 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() || 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) {
+        my $path = builtDylibPathForName($framework);
+        die "Can't find built framework at \"$path\".\n" unless -e $path;
+    }
+}
+
+sub isInspectorFrontend()
+{
+    determineIsInspectorFrontend();
+    return $isInspectorFrontend;
+}
+
+sub determineIsInspectorFrontend()
+{
+    return if defined($isInspectorFrontend);
+    $isInspectorFrontend = checkForArgumentAndRemoveFromARGV("--inspector-frontend");
+}
+
+sub isQt()
+{
+    determineIsQt();
+    return $isQt;
+}
+
+sub getQtVersion()
+{
+    my $qtVersion = `$qmakebin --version`;
+    $qtVersion =~ s/^(.*)Qt version (\d\.\d)(.*)/$2/s ;
+    return $qtVersion;
+}
+
+sub qtFeatureDefaults
+{
+    die "ERROR: qmake missing but required to build WebKit.\n" if not commandExists($qmakebin);
+
+    my $oldQmakeEval = $ENV{QMAKE_CACHE_EVAL};
+    $ENV{QMAKE_CACHE_EVAL} = "CONFIG+=print_defaults";
+
+    my $originalCwd = getcwd();
+    my $qmakepath = File::Spec->catfile(sourceDir(), "Tools", "qmake");
+    chdir $qmakepath or die "Failed to cd into " . $qmakepath . "\n";
+
+    my $file = File::Spec->catfile(sourceDir(), "WebKit.pro");
+
+    my @buildArgs;
+    @buildArgs = (@buildArgs, @{$_[0]}) if (@_);
+
+    my @defaults = `$qmakebin @buildArgs $file 2>&1`;
+
+    my %qtFeatureDefaults;
+    for (@defaults) {
+        if (/DEFINES: /) {
+            while (/(\S+?)=(\S+?)/gi) {
+                $qtFeatureDefaults{$1}=$2;
+            }
+        } elsif (/Done computing defaults/) {
+            last;
+        } elsif (@_) {
+            print $_;
+        }
+    }
+
+    chdir $originalCwd;
+    $ENV{QMAKE_CACHE_EVAL} = $oldQmakeEval;
+
+    return %qtFeatureDefaults;
+}
+
+sub commandExists($)
+{
+    my $command = shift;
+    my $devnull = File::Spec->devnull();
+    return `$command --version 2> $devnull`;
+}
+
+sub checkForArgumentAndRemoveFromARGV
+{
+    my $argToCheck = shift;
+    return checkForArgumentAndRemoveFromArrayRef($argToCheck, \@ARGV);
+}
+
+sub checkForArgumentAndRemoveFromArrayRef
+{
+    my ($argToCheck, $arrayRef) = @_;
+    my @indicesToRemove;
+    foreach my $index (0 .. $#$arrayRef) {
+        my $opt = $$arrayRef[$index];
+        if ($opt =~ /^$argToCheck$/i ) {
+            push(@indicesToRemove, $index);
+        }
+    }
+    foreach my $index (@indicesToRemove) {
+        splice(@$arrayRef, $index, 1);
+    }
+    return $#indicesToRemove > -1;
+}
+
+sub isWK2()
+{
+    if (defined($isWK2)) {
+        return $isWK2;
+    }
+    if (checkForArgumentAndRemoveFromARGV("-2")) {
+        $isWK2 = 1;
+    } else {
+        $isWK2 = 0;
+    }
+    return $isWK2;
+}
+
+sub determineIsQt()
+{
+    return if defined($isQt);
+
+    # Allow override in case QTDIR is not set.
+    if (checkForArgumentAndRemoveFromARGV("--qt")) {
+        $isQt = 1;
+        return;
+    }
+
+    # The presence of QTDIR only means Qt if --gtk or --wx or --efl or --blackberry or --chromium or --wincairo are not on the command-line
+    if (isGtk() || isWx() || isEfl() || isBlackBerry() || isChromium() || isWinCairo()) {
+        $isQt = 0;
+        return;
+    }
+
+    $isQt = defined($ENV{'QTDIR'});
+}
+
+sub isBlackBerry()
+{
+    determineIsBlackBerry();
+    return $isBlackBerry;
+}
+
+sub determineIsBlackBerry()
+{
+    return if defined($isBlackBerry);
+    $isBlackBerry = checkForArgumentAndRemoveFromARGV("--blackberry");
+}
+
+sub blackberryTargetArchitecture()
+{
+    my $arch = $ENV{"BLACKBERRY_ARCH_TYPE"} ? $ENV{"BLACKBERRY_ARCH_TYPE"} : "arm";
+    my $cpu = $ENV{"BLACKBERRY_ARCH_CPU"} ? $ENV{"BLACKBERRY_ARCH_CPU"} : "";
+    my $cpuDir;
+    my $buSuffix;
+    if (($cpu eq "v7le") || ($cpu eq "a9")) {
+        $cpuDir = $arch . "le-v7";
+        $buSuffix = $arch . "v7";
+    } else {
+        $cpu = $arch;
+        $cpuDir = $arch;
+        $buSuffix = $arch;
+    }
+    return ("arch" => $arch,
+            "cpu" => $cpu,
+            "cpuDir" => $cpuDir,
+            "buSuffix" => $buSuffix);
+}
+
+sub blackberryCMakeArguments()
+{
+    my %archInfo = blackberryTargetArchitecture();
+    my $arch = $archInfo{"arch"};
+    my $cpu = $archInfo{"cpu"};
+    my $cpuDir = $archInfo{"cpuDir"};
+    my $buSuffix = $archInfo{"buSuffix"};
+
+    my @cmakeExtraOptions;
+    if ($cpu eq "a9") {
+        $cpu = $arch . "v7le";
+        push @cmakeExtraOptions, '-DTARGETING_PLAYBOOK=1';
+    }
+
+    my $stageDir = $ENV{"STAGE_DIR"};
+    my $stageLib = File::Spec->catdir($stageDir, $cpuDir, "lib");
+    my $stageUsrLib = File::Spec->catdir($stageDir, $cpuDir, "usr", "lib");
+    my $stageInc = File::Spec->catdir($stageDir, "usr", "include");
+
+    my $qnxHost = $ENV{"QNX_HOST"};
+    my $ccCommand;
+    my $cxxCommand;
+    if ($ENV{"USE_ICECC"}) {
+        chomp($ccCommand = `which icecc`);
+        $cxxCommand = $ccCommand;
+    } else {
+        $ccCommand = File::Spec->catfile($qnxHost, "usr", "bin", "qcc");
+        $cxxCommand = $ccCommand;
+    }
+
+    if ($ENV{"CCWRAP"}) {
+        $ccCommand = $ENV{"CCWRAP"};
+        push @cmakeExtraOptions, "-DCMAKE_C_COMPILER_ARG1=qcc";
+        push @cmakeExtraOptions, "-DCMAKE_CXX_COMPILER_ARG1=qcc";
+    }
+
+    push @cmakeExtraOptions, "-DCMAKE_SKIP_RPATH='ON'" if isDarwin();
+    push @cmakeExtraOptions, "-DPUBLIC_BUILD=1" if $ENV{"PUBLIC_BUILD"};
+    push @cmakeExtraOptions, "-DENABLE_GLES2=1" unless $ENV{"DISABLE_GLES2"};
+
+    my @includeSystemDirectories;
+    push @includeSystemDirectories, File::Spec->catdir($stageInc, "grskia", "skia");
+    push @includeSystemDirectories, File::Spec->catdir($stageInc, "grskia");
+    push @includeSystemDirectories, File::Spec->catdir($stageInc, "harfbuzz");
+    push @includeSystemDirectories, File::Spec->catdir($stageInc, "imf");
+    push @includeSystemDirectories, $stageInc;
+    push @includeSystemDirectories, File::Spec->catdir($stageInc, "browser", "platform");
+    push @includeSystemDirectories, File::Spec->catdir($stageInc, "browser", "qsk");
+    push @includeSystemDirectories, File::Spec->catdir($stageInc, "ots");
+
+    my @cxxFlags;
+    push @cxxFlags, "-Wl,-rpath-link,$stageLib";
+    push @cxxFlags, "-Wl,-rpath-link," . File::Spec->catfile($stageUsrLib, "torch-webkit");
+    push @cxxFlags, "-Wl,-rpath-link,$stageUsrLib";
+    push @cxxFlags, "-L$stageLib";
+    push @cxxFlags, "-L$stageUsrLib";
+
+    if ($ENV{"PROFILE"}) {
+        push @cmakeExtraOptions, "-DPROFILING=1";
+        push @cxxFlags, "-p";
+    }
+
+    my @cmakeArgs;
+    push @cmakeArgs, '-DCMAKE_SYSTEM_NAME="QNX"';
+    push @cmakeArgs, "-DCMAKE_SYSTEM_PROCESSOR=\"$cpuDir\"";
+    push @cmakeArgs, '-DCMAKE_SYSTEM_VERSION="1"';
+    push @cmakeArgs, "-DCMAKE_C_COMPILER=\"$ccCommand\"";
+    push @cmakeArgs, "-DCMAKE_CXX_COMPILER=\"$cxxCommand\"";
+    push @cmakeArgs, "-DCMAKE_C_FLAGS=\"-Vgcc_nto${cpu} -g @cxxFlags\"";
+    push @cmakeArgs, "-DCMAKE_CXX_FLAGS=\"-Vgcc_nto${cpu}_cpp-ne -g -lang-c++ @cxxFlags\"";
+
+    # We cannot use CMAKE_INCLUDE_PATH since this describes the search path for header files in user directories.
+    # And the QNX system headers are in user directories on the host OS (i.e. they aren't installed in the host OS's
+    # system header search path). So, we need to inform g++ that these user directories (@includeSystemDirectories)
+    # are to be taken as the host OS's system header directories when building our port.
+    #
+    # Also, we cannot use CMAKE_SYSTEM_INCLUDE_PATH since that will override the entire system header path.
+    # So, we define the additional system include paths in ADDITIONAL_SYSTEM_INCLUDE_PATH. This list will
+    # be processed in OptionsBlackBerry.cmake.
+    push @cmakeArgs, '-DADDITIONAL_SYSTEM_INCLUDE_PATH="' . join(';', @includeSystemDirectories) . '"';
+
+    # FIXME: Make this more general purpose such that we can pass a list of directories and files.
+    push @cmakeArgs, '-DTHIRD_PARTY_ICU_DIR="' . File::Spec->catdir($stageInc, "unicode") . '"';
+    push @cmakeArgs, '-DTHIRD_PARTY_UNICODE_FILE="' . File::Spec->catfile($stageInc, "unicode.h") . '"';
+
+    push @cmakeArgs, "-DCMAKE_LIBRARY_PATH=\"$stageLib;$stageUsrLib\"";
+    push @cmakeArgs, '-DCMAKE_AR="' . File::Spec->catfile($qnxHost, "usr", "bin", "nto${buSuffix}-ar") . '"';
+    push @cmakeArgs, '-DCMAKE_RANLIB="' . File::Spec->catfile($qnxHost, "usr", "bin", "nto${buSuffix}-ranlib") . '"';
+    push @cmakeArgs, '-DCMAKE_LD="'. File::Spec->catfile($qnxHost, "usr", "bin", "nto${buSuffix}-ld") . '"';
+    push @cmakeArgs, '-DCMAKE_LINKER="' . File::Spec->catfile($qnxHost, "usr", "bin", "nto${buSuffix}-ld") . '"';
+    push @cmakeArgs, "-DECLIPSE_CDT4_GENERATE_SOURCE_PROJECT=TRUE";
+    push @cmakeArgs, '-G"Eclipse CDT4 - Unix Makefiles"';
+    push @cmakeArgs, @cmakeExtraOptions;
+    return @cmakeArgs;
+}
+
+sub determineIsEfl()
+{
+    return if defined($isEfl);
+    $isEfl = checkForArgumentAndRemoveFromARGV("--efl");
+}
+
+sub isEfl()
+{
+    determineIsEfl();
+    return $isEfl;
+}
+
+sub isGtk()
+{
+    determineIsGtk();
+    return $isGtk;
+}
+
+sub determineIsGtk()
+{
+    return if defined($isGtk);
+    $isGtk = checkForArgumentAndRemoveFromARGV("--gtk");
+}
+
+sub isWinCE()
+{
+    determineIsWinCE();
+    return $isWinCE;
+}
+
+sub determineIsWinCE()
+{
+    return if defined($isWinCE);
+    $isWinCE = checkForArgumentAndRemoveFromARGV("--wince");
+}
+
+sub isWx()
+{
+    determineIsWx();
+    return $isWx;
+}
+
+sub determineIsWx()
+{
+    return if defined($isWx);
+    $isWx = checkForArgumentAndRemoveFromARGV("--wx");
+}
+
+sub getWxArgs()
+{
+    if (!@wxArgs) {
+        @wxArgs = ("");
+        my $rawWxArgs = "";
+        foreach my $opt (@ARGV) {
+            if ($opt =~ /^--wx-args/i ) {
+                @ARGV = grep(!/^--wx-args/i, @ARGV);
+                $rawWxArgs = $opt;
+                $rawWxArgs =~ s/--wx-args=//i;
+            }
+        }
+        @wxArgs = split(/,/, $rawWxArgs);
+    }
+    return @wxArgs;
+}
+
+# Determine if this is debian, ubuntu, linspire, or something similar.
+sub isDebianBased()
+{
+    return -e "/etc/debian_version";
+}
+
+sub isFedoraBased()
+{
+    return -e "/etc/fedora-release";
+}
+
+sub isChromium()
+{
+    determineIsChromium();
+    determineIsChromiumAndroid();
+    return $isChromium || $isChromiumAndroid;
+}
+
+sub determineIsChromium()
+{
+    return if defined($isChromium);
+    $isChromium = checkForArgumentAndRemoveFromARGV("--chromium");
+    if ($isChromium) {
+        $forceChromiumUpdate = checkForArgumentAndRemoveFromARGV("--force-update");
+    }
+}
+
+sub isChromiumAndroid()
+{
+    determineIsChromiumAndroid();
+    return $isChromiumAndroid;
+}
+
+sub determineIsChromiumAndroid()
+{
+    return if defined($isChromiumAndroid);
+    $isChromiumAndroid = checkForArgumentAndRemoveFromARGV("--chromium-android");
+}
+
+sub isChromiumMacMake()
+{
+    determineIsChromiumMacMake();
+    return $isChromiumMacMake;
+}
+
+sub determineIsChromiumMacMake()
+{
+    return if defined($isChromiumMacMake);
+
+    my $hasUpToDateMakefile = 0;
+    if (-e 'Makefile.chromium') {
+        unless (-e 'Source/WebKit/chromium/WebKit.xcodeproj') {
+            $hasUpToDateMakefile = 1;
+        } else {
+            $hasUpToDateMakefile = stat('Makefile.chromium')->mtime > stat('Source/WebKit/chromium/WebKit.xcodeproj')->mtime;
+        }
+    }
+    $isChromiumMacMake = isDarwin() && $hasUpToDateMakefile;
+}
+
+sub isChromiumNinja()
+{
+    determineIsChromiumNinja();
+    return $isChromiumNinja;
+}
+
+sub determineIsChromiumNinja()
+{
+    return if defined($isChromiumNinja);
+
+    my $config = configuration();
+
+    my $hasUpToDateNinjabuild = 0;
+    if (-e "out/$config/build.ninja") {
+        my $statNinja = stat("out/$config/build.ninja")->mtime;
+
+        my $statXcode = 0;
+        if (-e 'Source/WebKit/chromium/WebKit.xcodeproj') {
+          $statXcode = stat('Source/WebKit/chromium/WebKit.xcodeproj')->mtime;
+        }
+
+        my $statMake = 0;
+        if (-e 'Makefile.chromium') {
+          $statMake = stat('Makefile.chromium')->mtime;
+        }
+
+        $hasUpToDateNinjabuild = $statNinja > $statXcode && $statNinja > $statMake;
+    }
+    $isChromiumNinja = $hasUpToDateNinjabuild;
+}
+
+sub forceChromiumUpdate()
+{
+    determineIsChromium();
+    return $forceChromiumUpdate;
+}
+
+sub isWinCairo()
+{
+    determineIsWinCairo();
+    return $isWinCairo;
+}
+
+sub determineIsWinCairo()
+{
+    return if defined($isWinCairo);
+    $isWinCairo = checkForArgumentAndRemoveFromARGV("--wincairo");
+}
+
+sub isCygwin()
+{
+    return ($^O eq "cygwin") || 0;
+}
+
+sub isAnyWindows()
+{
+    return isWindows() || isCygwin();
+}
+
+sub determineWinVersion()
+{
+    return if $winVersion;
+
+    if (!isAnyWindows()) {
+        $winVersion = -1;
+        return;
+    }
+
+    my $versionString = `cmd /c ver`;
+    $versionString =~ /(\d)\.(\d)\.(\d+)/;
+
+    $winVersion = {
+        major => $1,
+        minor => $2,
+        build => $3,
+    };
+}
+
+sub winVersion()
+{
+    determineWinVersion();
+    return $winVersion;
+}
+
+sub isWindows7SP0()
+{
+    return isAnyWindows() && winVersion()->{major} == 6 && winVersion()->{minor} == 1 && winVersion()->{build} == 7600;
+}
+
+sub isWindowsVista()
+{
+    return isAnyWindows() && winVersion()->{major} == 6 && winVersion()->{minor} == 0;
+}
+
+sub isWindowsXP()
+{
+    return isAnyWindows() && winVersion()->{major} == 5 && winVersion()->{minor} == 1;
+}
+
+sub isDarwin()
+{
+    return ($^O eq "darwin") || 0;
+}
+
+sub isWindows()
+{
+    return ($^O eq "MSWin32") || 0;
+}
+
+sub isLinux()
+{
+    return ($^O eq "linux") || 0;
+}
+
+sub isFreeBSD()
+{
+    return ($^O eq "freebsd") || 0;
+}
+
+sub isARM()
+{
+    return $Config{archname} =~ /^arm-/;
+}
+
+sub isCrossCompilation()
+{
+  my $compiler = "";
+  $compiler = $ENV{'CC'} if (defined($ENV{'CC'}));
+  if ($compiler =~ /gcc/) {
+      my $compiler_options = `$compiler -v 2>&1`;
+      my @host = $compiler_options =~ m/--host=(.*?)\s/;
+      my @target = $compiler_options =~ m/--target=(.*?)\s/;
+
+      return ($host[0] ne "" && $target[0] ne "" && $host[0] ne $target[0]);
+  }
+  return 0;
+}
+
+sub isAppleWebKit()
+{
+    return !(isQt() or isGtk() or isWx() or isChromium() or isEfl() or isWinCE() or isBlackBerry());
+}
+
+sub isAppleMacWebKit()
+{
+    return isAppleWebKit() && isDarwin();
+}
+
+sub isAppleWinWebKit()
+{
+    return isAppleWebKit() && (isCygwin() || isWindows());
+}
+
+sub isPerianInstalled()
+{
+    if (!isAppleWebKit()) {
+        return 0;
+    }
+
+    if (-d "/Library/QuickTime/Perian.component") {
+        return 1;
+    }
+
+    if (-d "$ENV{HOME}/Library/QuickTime/Perian.component") {
+        return 1;
+    }
+
+    return 0;
+}
+
+sub determineNmPath()
+{
+    return if $nmPath;
+
+    if (isAppleMacWebKit()) {
+        $nmPath = `xcrun -find nm`;
+        chomp $nmPath;
+    }
+    $nmPath = "nm" if !$nmPath;
+}
+
+sub nmPath()
+{
+    determineNmPath();
+    return $nmPath;
+}
+
+sub determineOSXVersion()
+{
+    return if $osXVersion;
+
+    if (!isDarwin()) {
+        $osXVersion = -1;
+        return;
+    }
+
+    my $version = `sw_vers -productVersion`;
+    my @splitVersion = split(/\./, $version);
+    @splitVersion >= 2 or die "Invalid version $version";
+    $osXVersion = {
+            "major" => $splitVersion[0],
+            "minor" => $splitVersion[1],
+            "subminor" => (defined($splitVersion[2]) ? $splitVersion[2] : 0),
+    };
+}
+
+sub osXVersion()
+{
+    determineOSXVersion();
+    return $osXVersion;
+}
+
+sub isSnowLeopard()
+{
+    return isDarwin() && osXVersion()->{"minor"} == 6;
+}
+
+sub isLion()
+{
+    return isDarwin() && osXVersion()->{"minor"} == 7;
+}
+
+sub isWindowsNT()
+{
+    return $ENV{'OS'} eq 'Windows_NT';
+}
+
+sub shouldTargetWebProcess
+{
+    determineShouldTargetWebProcess();
+    return $shouldTargetWebProcess;
+}
+
+sub determineShouldTargetWebProcess
+{
+    return if defined($shouldTargetWebProcess);
+    $shouldTargetWebProcess = checkForArgumentAndRemoveFromARGV("--target-web-process");
+}
+
+sub shouldUseXPCServiceForWebProcess
+{
+    determineShouldUseXPCServiceForWebProcess();
+    return $shouldUseXPCServiceForWebProcess;
+}
+
+sub determineShouldUseXPCServiceForWebProcess
+{
+    return if defined($shouldUseXPCServiceForWebProcess);
+    $shouldUseXPCServiceForWebProcess = checkForArgumentAndRemoveFromARGV("--use-web-process-xpc-service");
+}
+
+sub debugger
+{
+    determineDebugger();
+    return $debugger;
+}
+
+sub determineDebugger
+{
+    return if defined($debugger);
+
+    determineXcodeVersion();
+    if (eval "v$xcodeVersion" ge v4.5) {
+        $debugger = "lldb";
+    } else {
+        $debugger = "gdb";
+    }
+
+    if (checkForArgumentAndRemoveFromARGV("--use-lldb")) {
+        $debugger = "lldb";
+    }
+
+    if (checkForArgumentAndRemoveFromARGV("--use-gdb")) {
+        $debugger = "gdb";
+    }
+}
+
+sub appendToEnvironmentVariableList
+{
+    my ($environmentVariableName, $value) = @_;
+
+    if (defined($ENV{$environmentVariableName})) {
+        $ENV{$environmentVariableName} .= ":" . $value;
+    } else {
+        $ENV{$environmentVariableName} = $value;
+    }
+}
+
+sub setUpGuardMallocIfNeeded
+{
+    if (!isDarwin()) {
+        return;
+    }
+
+    if (!defined($shouldUseGuardMalloc)) {
+        $shouldUseGuardMalloc = checkForArgumentAndRemoveFromARGV("--guard-malloc");
+    }
+
+    if ($shouldUseGuardMalloc) {
+        appendToEnvironmentVariableList("DYLD_INSERT_LIBRARIES", "/usr/lib/libgmalloc.dylib");
+    }
+}
+
+sub relativeScriptsDir()
+{
+    my $scriptDir = File::Spec->catpath("", File::Spec->abs2rel($FindBin::Bin, getcwd()), "");
+    if ($scriptDir eq "") {
+        $scriptDir = ".";
+    }
+    return $scriptDir;
+}
+
+sub launcherPath()
+{
+    my $relativeScriptsPath = relativeScriptsDir();
+    if (isGtk() || isQt() || isWx() || isEfl() || isWinCE()) {
+        return "$relativeScriptsPath/run-launcher";
+    } elsif (isAppleWebKit()) {
+        return "$relativeScriptsPath/run-safari";
+    }
+}
+
+sub launcherName()
+{
+    if (isGtk()) {
+        return "GtkLauncher";
+    } elsif (isQt()) {
+        return "QtTestBrowser";
+    } elsif (isWx()) {
+        return "wxBrowser";
+    } elsif (isAppleWebKit()) {
+        return "Safari";
+    } elsif (isEfl()) {
+        return "EWebLauncher";
+    } elsif (isWinCE()) {
+        return "WinCELauncher";
+    }
+}
+
+sub checkRequiredSystemConfig
+{
+    if (isDarwin()) {
+        chomp(my $productVersion = `sw_vers -productVersion`);
+        if (eval "v$productVersion" lt v10.4) {
+            print "*************************************************************\n";
+            print "Mac OS X Version 10.4.0 or later is required to build WebKit.\n";
+            print "You have " . $productVersion . ", thus the build will most likely fail.\n";
+            print "*************************************************************\n";
+        }
+        my $xcodebuildVersionOutput = `xcodebuild -version`;
+        my $devToolsCoreVersion = ($xcodebuildVersionOutput =~ /DevToolsCore-(\d+)/) ? $1 : undef;
+        my $xcodeVersion = ($xcodebuildVersionOutput =~ /Xcode ([0-9](\.[0-9]+)*)/) ? $1 : undef;
+        if (!$devToolsCoreVersion && !$xcodeVersion
+            || $devToolsCoreVersion && $devToolsCoreVersion < 747
+            || $xcodeVersion && eval "v$xcodeVersion" lt v2.3) {
+            print "*************************************************************\n";
+            print "Xcode Version 2.3 or later is required to build WebKit.\n";
+            print "You have an earlier version of Xcode, thus the build will\n";
+            print "most likely fail.  The latest Xcode is available from the web:\n";
+            print "http://developer.apple.com/tools/xcode\n";
+            print "*************************************************************\n";
+        }
+    } elsif (isGtk() or isQt() or isWx() or isEfl()) {
+        my @cmds = qw(bison gperf);
+        if (isQt() and isWindows()) {
+            push @cmds, "win_flex";
+        } else {
+            push @cmds, "flex";
+        }
+        my @missing = ();
+        my $oldPath = $ENV{PATH};
+        if (isQt() and isWindows()) {
+            chomp(my $gnuWin32Dir = `$qmakebin -query QT_HOST_DATA`);
+            $gnuWin32Dir = File::Spec->catfile($gnuWin32Dir, "..", "gnuwin32", "bin");
+            if (-d "$gnuWin32Dir") {
+                $ENV{PATH} = $gnuWin32Dir . ";" . $ENV{PATH};
+            }
+        }
+        foreach my $cmd (@cmds) {
+            push @missing, $cmd if not commandExists($cmd);
+        }
+
+        if (@missing) {
+            my $list = join ", ", @missing;
+            die "ERROR: $list missing but required to build WebKit.\n";
+        }
+        if (isQt() and isWindows()) {
+            $ENV{PATH} = $oldPath;
+        }
+    }
+    # Win32 and other platforms may want to check for minimum config
+}
+
+sub determineWindowsSourceDir()
+{
+    return if $windowsSourceDir;
+    $windowsSourceDir = sourceDir();
+    chomp($windowsSourceDir = `cygpath -w '$windowsSourceDir'`) if isCygwin();
+}
+
+sub windowsSourceDir()
+{
+    determineWindowsSourceDir();
+    return $windowsSourceDir;
+}
+
+sub windowsLibrariesDir()
+{
+    return windowsSourceDir() . "\\WebKitLibraries\\win";
+}
+
+sub windowsOutputDir()
+{
+    return windowsSourceDir() . "\\WebKitBuild";
+}
+
+sub setupAppleWinEnv()
+{
+    return unless isAppleWinWebKit();
+
+    if (isWindowsNT()) {
+        my $restartNeeded = 0;
+        my %variablesToSet = ();
+
+        # FIXME: We should remove this explicit version check for cygwin once we stop supporting Cygwin 1.7.9 or older versions. 
+        # https://bugs.webkit.org/show_bug.cgi?id=85791
+        my $uname_version = (POSIX::uname())[2];
+        $uname_version =~ s/\(.*\)//;  # Remove the trailing cygwin version, if any.
+        if (version->parse($uname_version) < version->parse("1.7.10")) {
+            # Setting the environment variable 'CYGWIN' to 'tty' makes cygwin enable extra support (i.e., termios)
+            # for UNIX-like ttys in the Windows console
+            $variablesToSet{CYGWIN} = "tty" unless $ENV{CYGWIN};
+        }
+        
+        # Those environment variables must be set to be able to build inside Visual Studio.
+        $variablesToSet{WEBKITLIBRARIESDIR} = windowsLibrariesDir() unless $ENV{WEBKITLIBRARIESDIR};
+        $variablesToSet{WEBKITOUTPUTDIR} = windowsOutputDir() unless $ENV{WEBKITOUTPUTDIR};
+
+        foreach my $variable (keys %variablesToSet) {
+            print "Setting the Environment Variable '" . $variable . "' to '" . $variablesToSet{$variable} . "'\n\n";
+            system qw(regtool -s set), '\\HKEY_CURRENT_USER\\Environment\\' . $variable, $variablesToSet{$variable};
+            $restartNeeded ||= $variable eq "WEBKITLIBRARIESDIR" || $variable eq "WEBKITOUTPUTDIR";
+        }
+
+        if ($restartNeeded) {
+            print "Please restart your computer before attempting to build inside Visual Studio.\n\n";
+        }
+    } else {
+        if (!$ENV{'WEBKITLIBRARIESDIR'}) {
+            print "Warning: You must set the 'WebKitLibrariesDir' environment variable\n";
+            print "         to be able build WebKit from within Visual Studio.\n";
+            print "         Make sure that 'WebKitLibrariesDir' points to the\n";
+            print "         'WebKitLibraries/win' directory, not the 'WebKitLibraries/' directory.\n\n";
+        }
+        if (!$ENV{'WEBKITOUTPUTDIR'}) {
+            print "Warning: You must set the 'WebKitOutputDir' environment variable\n";
+            print "         to be able build WebKit from within Visual Studio.\n\n";
+        }
+    }
+}
+
+sub setupCygwinEnv()
+{
+    return if !isCygwin() && !isWindows();
+    return if $vcBuildPath;
+
+    my $vsInstallDir;
+    my $programFilesPath = $ENV{'PROGRAMFILES(X86)'} || $ENV{'PROGRAMFILES'} || "C:\\Program Files";
+    if ($ENV{'VSINSTALLDIR'}) {
+        $vsInstallDir = $ENV{'VSINSTALLDIR'};
+    } else {
+        $vsInstallDir = File::Spec->catdir($programFilesPath, "Microsoft Visual Studio 8");
+    }
+    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.
+        # FIXME: Make pdevenv work with non-Cygwin Perl.
+        $vcBuildPath = File::Spec->catfile(sourceDir(), qw(Tools Scripts pdevenv)) if isCygwin();
+    } else {
+        # Visual Studio not found, try VC++ Express
+        $vcBuildPath = File::Spec->catfile($vsInstallDir, qw(Common7 IDE VCExpress.exe));
+        if (! -e $vcBuildPath) {
+            print "*************************************************************\n";
+            print "Cannot find '$vcBuildPath'\n";
+            print "Please execute the file 'vcvars32.bat' from\n";
+            print "'$programFilesPath\\Microsoft Visual Studio 8\\VC\\bin\\'\n";
+            print "to setup the necessary environment variables.\n";
+            print "*************************************************************\n";
+            die;
+        }
+        $willUseVCExpressWhenBuilding = 1;
+    }
+
+    my $qtSDKPath = File::Spec->catdir($programFilesPath, "QuickTime SDK");
+    if (0 && ! -e $qtSDKPath) {
+        print "*************************************************************\n";
+        print "Cannot find '$qtSDKPath'\n";
+        print "Please download the QuickTime SDK for Windows from\n";
+        print "http://developer.apple.com/quicktime/download/\n";
+        print "*************************************************************\n";
+        die;
+    }
+    
+    unless ($ENV{WEBKITLIBRARIESDIR}) {
+        $ENV{'WEBKITLIBRARIESDIR'} = File::Spec->catdir($sourceDir, "WebKitLibraries", "win");
+        chomp($ENV{WEBKITLIBRARIESDIR} = `cygpath -wa '$ENV{WEBKITLIBRARIESDIR}'`) if isCygwin();
+    }
+
+    print "Building results into: ", baseProductDir(), "\n";
+    print "WEBKITOUTPUTDIR is set to: ", $ENV{"WEBKITOUTPUTDIR"}, "\n";
+    print "WEBKITLIBRARIESDIR is set to: ", $ENV{"WEBKITLIBRARIESDIR"}, "\n";
+}
+
+sub dieIfWindowsPlatformSDKNotInstalled
+{
+    my $registry32Path = "/proc/registry/";
+    my $registry64Path = "/proc/registry64/";
+    my $windowsPlatformSDKRegistryEntry = "HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/MicrosoftSDK/InstalledSDKs/D2FF9F89-8AA2-4373-8A31-C838BF4DBBE1";
+
+    # FIXME: It would be better to detect whether we are using 32- or 64-bit Windows
+    # and only check the appropriate entry. But for now we just blindly check both.
+    return if (-e $registry32Path . $windowsPlatformSDKRegistryEntry) || (-e $registry64Path . $windowsPlatformSDKRegistryEntry);
+
+    print "*************************************************************\n";
+    print "Cannot find registry entry '$windowsPlatformSDKRegistryEntry'.\n";
+    print "Please download and install the Microsoft Windows Server 2003 R2\n";
+    print "Platform SDK from <http://www.microsoft.com/downloads/details.aspx?\n";
+    print "familyid=0baf2b35-c656-4969-ace8-e4c0c0716adb&displaylang=en>.\n\n";
+    print "Then follow step 2 in the Windows section of the \"Installing Developer\n";
+    print "Tools\" instructions at <http://www.webkit.org/building/tools.html>.\n";
+    print "*************************************************************\n";
+    die;
+}
+
+sub copyInspectorFrontendFiles
+{
+    my $productDir = productDir();
+    my $sourceInspectorPath = sourceDir() . "/Source/WebCore/inspector/front-end/";
+    my $inspectorResourcesDirPath = $ENV{"WEBKITINSPECTORRESOURCESDIR"};
+
+    if (!defined($inspectorResourcesDirPath)) {
+        $inspectorResourcesDirPath = "";
+    }
+
+    if (isAppleMacWebKit()) {
+        $inspectorResourcesDirPath = $productDir . "/WebCore.framework/Resources/inspector";
+    } elsif (isAppleWinWebKit()) {
+        $inspectorResourcesDirPath = $productDir . "/WebKit.resources/inspector";
+    } elsif (isQt() || isGtk()) {
+        my $prefix = $ENV{"WebKitInstallationPrefix"};
+        $inspectorResourcesDirPath = (defined($prefix) ? $prefix : "/usr/share") . "/webkit-1.0/webinspector";
+    } elsif (isEfl()) {
+        my $prefix = $ENV{"WebKitInstallationPrefix"};
+        $inspectorResourcesDirPath = (defined($prefix) ? $prefix : "/usr/share") . "/ewebkit/webinspector";
+    }
+
+    if (! -d $inspectorResourcesDirPath) {
+        print "*************************************************************\n";
+        print "Cannot find '$inspectorResourcesDirPath'.\n" if (defined($inspectorResourcesDirPath));
+        print "Make sure that you have built WebKit first.\n" if (! -d $productDir || defined($inspectorResourcesDirPath));
+        print "Optionally, set the environment variable 'WebKitInspectorResourcesDir'\n";
+        print "to point to the directory that contains the WebKit Inspector front-end\n";
+        print "files for the built WebCore framework.\n";
+        print "*************************************************************\n";
+        die;
+    }
+
+    if (isAppleMacWebKit()) {
+        my $sourceLocalizedStrings = sourceDir() . "/Source/WebCore/English.lproj/localizedStrings.js";
+        my $destinationLocalizedStrings = $productDir . "/WebCore.framework/Resources/English.lproj/localizedStrings.js";
+        system "ditto", $sourceLocalizedStrings, $destinationLocalizedStrings;
+    }
+
+    return system "rsync", "-aut", "--exclude=/.DS_Store", "--exclude=*.re2js", "--exclude=.svn/", !isQt() ? "--exclude=/WebKit.qrc" : "", $sourceInspectorPath, $inspectorResourcesDirPath;
+}
+
+sub buildXCodeProject($$@)
+{
+    my ($project, $clean, @extraOptions) = @_;
+
+    if ($clean) {
+        push(@extraOptions, "-alltargets");
+        push(@extraOptions, "clean");
+    }
+
+    return system "xcodebuild", "-project", "$project.xcodeproj", @extraOptions;
+}
+
+sub usingVisualStudioExpress()
+{
+    setupCygwinEnv();
+    return $willUseVCExpressWhenBuilding;
+}
+
+sub buildVisualStudioProject
+{
+    my ($project, $clean) = @_;
+    setupCygwinEnv();
+
+    my $config = configurationForVisualStudio();
+
+    dieIfWindowsPlatformSDKNotInstalled() if $willUseVCExpressWhenBuilding;
+
+    chomp($project = `cygpath -w "$project"`) if isCygwin();
+    
+    my $action = "/build";
+    if ($clean) {
+        $action = "/clean";
+    }
+
+    my @command = ($vcBuildPath, $project, $action, $config);
+
+    print join(" ", @command), "\n";
+    return system @command;
+}
+
+sub downloadWafIfNeeded
+{
+    # get / update waf if needed
+    my $waf = "$sourceDir/Tools/waf/waf";
+    my $wafURL = 'http://wxwebkit.kosoftworks.com/downloads/deps/waf';
+    if (!-f $waf) {
+        my $result = system "curl -o $waf $wafURL";
+        chmod 0755, $waf;
+    }
+}
+
+sub buildWafProject
+{
+    my ($project, $shouldClean, @options) = @_;
+    
+    # set the PYTHONPATH for waf
+    my $pythonPath = $ENV{'PYTHONPATH'};
+    if (!defined($pythonPath)) {
+        $pythonPath = '';
+    }
+    my $sourceDir = sourceDir();
+    my $newPythonPath = "$sourceDir/Tools/waf/build:$pythonPath";
+    if (isCygwin()) {
+        $newPythonPath = `cygpath --mixed --path $newPythonPath`;
+    }
+    $ENV{'PYTHONPATH'} = $newPythonPath;
+    
+    print "Building $project\n";
+
+    my $wafCommand = "$sourceDir/Tools/waf/waf";
+    if ($ENV{'WXWEBKIT_WAF'}) {
+        $wafCommand = $ENV{'WXWEBKIT_WAF'};
+    }
+    if (isCygwin()) {
+        $wafCommand = `cygpath --windows "$wafCommand"`;
+        chomp($wafCommand);
+    }
+    if ($shouldClean) {
+        return system $wafCommand, "uninstall", "clean", "distclean";
+    }
+    
+    return system $wafCommand, 'configure', 'build', 'install', @options;
+}
+
+sub retrieveQMakespecVar
+{
+    my $mkspec = $_[0];
+    my $varname = $_[1];
+
+    my $varvalue = undef;
+    #print "retrieveMakespecVar " . $mkspec . ", " . $varname . "\n";
+
+    local *SPEC;
+    open SPEC, "<$mkspec" or return $varvalue;
+    while (<SPEC>) {
+        if ($_ =~ /\s*include\((.+)\)/) {
+            # open the included mkspec
+            my $oldcwd = getcwd();
+            (my $volume, my $directories, my $file) = File::Spec->splitpath($mkspec);
+            my $newcwd = "$volume$directories";
+            chdir $newcwd if $newcwd;
+            $varvalue = retrieveQMakespecVar($1, $varname);
+            chdir $oldcwd;
+        } elsif ($_ =~ /$varname\s*=\s*([^\s]+)/) {
+            $varvalue = $1;
+            last;
+        }
+    }
+    close SPEC;
+    return $varvalue;
+}
+
+sub qtMakeCommand($)
+{
+    my ($qmakebin) = @_;
+    chomp(my $hostDataPath = `$qmakebin -query QT_HOST_DATA`);
+    my $mkspecPath = $hostDataPath . "/mkspecs/default/qmake.conf";
+    if (! -e $mkspecPath) {
+        chomp(my $mkspec= `$qmakebin -query QMAKE_XSPEC`);
+        $mkspecPath = $hostDataPath . "/mkspecs/" . $mkspec . "/qmake.conf";
+    }
+    my $compiler = retrieveQMakespecVar($mkspecPath, "QMAKE_CC");
+
+    #print "default spec: " . $mkspec . "\n";
+    #print "compiler found: " . $compiler . "\n";
+
+    if ($compiler && $compiler eq "cl") {
+        return "nmake";
+    }
+
+    return "make";
+}
+
+sub autotoolsFlag($$)
+{
+    my ($flag, $feature) = @_;
+    my $prefix = $flag ? "--enable" : "--disable";
+
+    return $prefix . '-' . $feature;
+}
+
+sub runAutogenForAutotoolsProjectIfNecessary($@)
+{
+    my ($dir, $prefix, $sourceDir, $project, @buildArgs) = @_;
+
+    my $argumentsFile = "previous-autogen-arguments.txt";
+    if (-e "GNUmakefile") {
+        # Just assume that build-jsc will never be used to reconfigure JSC. Later
+        # we can go back and make this more complicated if the demand is there.
+        if ($project ne "WebKit") {
+            return;
+        }
+
+        # We only run autogen.sh again if the arguments passed have changed.
+        if (!mustReRunAutogen($sourceDir, $argumentsFile, @buildArgs)) {
+            return;
+        }
+    }
+
+    print "Calling autogen.sh in " . $dir . "\n\n";
+    print "Installation prefix directory: $prefix\n" if(defined($prefix));
+
+    # Only for WebKit, write the autogen.sh arguments to a file so that we can detect
+    # when they change and automatically re-run it.
+    if ($project eq 'WebKit') {
+        open(AUTOTOOLS_ARGUMENTS, ">$argumentsFile");
+        print AUTOTOOLS_ARGUMENTS join(" ", @buildArgs);
+        close(AUTOTOOLS_ARGUMENTS);
+    }
+
+    # Make the path relative since it will appear in all -I compiler flags.
+    # Long argument lists cause bizarre slowdowns in libtool.
+    my $relSourceDir = File::Spec->abs2rel($sourceDir) || ".";
+
+    # Compiler options to keep floating point values consistent
+    # between 32-bit and 64-bit architectures. The options are also
+    # used on Chromium build.
+    determineArchitecture();
+    if ($architecture ne "x86_64" && !isARM()) {
+        $ENV{'CXXFLAGS'} = "-march=pentium4 -msse2 -mfpmath=sse " . ($ENV{'CXXFLAGS'} || "");
+    }
+
+    # Prefix the command with jhbuild run.
+    unshift(@buildArgs, "$relSourceDir/autogen.sh");
+    my $jhbuildWrapperPrefix = jhbuildWrapperPrefixIfNeeded();
+    if ($jhbuildWrapperPrefix) {
+        unshift(@buildArgs, $jhbuildWrapperPrefix);
+    }
+    if (system(@buildArgs) ne 0) {
+        die "Calling autogen.sh failed!\n";
+    }
+}
+
+sub getJhbuildPath()
+{
+    return join('/', baseProductDir(), "Dependencies");
+}
+
+sub mustReRunAutogen($@)
+{
+    my ($sourceDir, $filename, @currentArguments) = @_;
+
+    if (! -e $filename) {
+        return 1;
+    }
+
+    open(AUTOTOOLS_ARGUMENTS, $filename);
+    chomp(my $previousArguments = <AUTOTOOLS_ARGUMENTS>);
+    close(AUTOTOOLS_ARGUMENTS);
+
+    # We only care about the WebKit2 argument when we are building WebKit itself.
+    # build-jsc never passes --enable-webkit2, so if we didn't do this, autogen.sh
+    # would run for every single build on the bots, since it runs both build-webkit
+    # and build-jsc.
+    my $joinedCurrentArguments = join(" ", @currentArguments);
+    if ($previousArguments ne $joinedCurrentArguments) {
+        print "Previous autogen arguments were: $previousArguments\n\n";
+        print "New autogen arguments are: $joinedCurrentArguments\n";
+        return 1;
+    }
+
+    return 0;
+}
+
+sub buildAutotoolsProject($@)
+{
+    my ($project, $clean, @buildParams) = @_;
+
+    my $make = 'make';
+    my $dir = productDir();
+    my $config = passedConfiguration() || configuration();
+    my $prefix;
+
+    # Use rm to clean the build directory since distclean may miss files
+    if ($clean && -d $dir) {
+        system "rm", "-rf", "$dir";
+    }
+
+    if (! -d $dir) {
+        File::Path::mkpath($dir) or die "Failed to create build directory " . $dir
+    }
+    chdir $dir or die "Failed to cd into " . $dir . "\n";
+
+    if ($clean) {
+        return 0;
+    }
+
+    my @buildArgs = ();
+    my $makeArgs = $ENV{"WebKitMakeArguments"} || "";
+    for my $i (0 .. $#buildParams) {
+        my $opt = $buildParams[$i];
+        if ($opt =~ /^--makeargs=(.*)/i ) {
+            $makeArgs = $makeArgs . " " . $1;
+        } elsif ($opt =~ /^--prefix=(.*)/i ) {
+            $prefix = $1;
+        } else {
+            push @buildArgs, $opt;
+        }
+    }
+
+    # Automatically determine the number of CPUs for make only
+    # if make arguments haven't already been specified.
+    if ($makeArgs eq "") {
+        $makeArgs = "-j" . numberOfCPUs();
+    }
+
+    # WebKit is the default target, so we don't need to specify anything.
+    if ($project eq "JavaScriptCore") {
+        $makeArgs .= " jsc";
+    } elsif ($project eq "WTF") {
+        $makeArgs .= " libWTF.la";
+    }
+
+    $prefix = $ENV{"WebKitInstallationPrefix"} if !defined($prefix);
+    push @buildArgs, "--prefix=" . $prefix if defined($prefix);
+
+    # Check if configuration is Debug.
+    my $debug = $config =~ m/debug/i;
+    if ($debug) {
+        push @buildArgs, "--enable-debug";
+    } else {
+        push @buildArgs, "--disable-debug";
+    }
+
+    # Enable unstable features when building through build-webkit.
+    push @buildArgs, "--enable-unstable-features";
+
+    if (checkForArgumentAndRemoveFromArrayRef("--update-gtk", \@buildArgs)) {
+        # Force autogen to run, to catch the possibly updated libraries.
+        system("rm -f previous-autogen-arguments.txt");
+
+        system("perl", "$sourceDir/Tools/Scripts/update-webkitgtk-libs") == 0 or die $!;
+    }
+
+    # If GNUmakefile exists, don't run autogen.sh unless its arguments
+    # have changed. The makefile should be smart enough to track autotools
+    # dependencies and re-run autogen.sh when build files change.
+    runAutogenForAutotoolsProjectIfNecessary($dir, $prefix, $sourceDir, $project, @buildArgs);
+
+    my $runWithJhbuild = jhbuildWrapperPrefixIfNeeded();
+    if (system("$runWithJhbuild $make $makeArgs") ne 0) {
+        die "\nFailed to build WebKit using '$make'!\n";
+    }
+
+    chdir ".." or die;
+
+    if ($project eq 'WebKit' && !isCrossCompilation()) {
+        my @docGenerationOptions = ("$sourceDir/Tools/gtk/generate-gtkdoc", "--skip-html");
+        push(@docGenerationOptions, productDir());
+
+        if ($runWithJhbuild) {
+            unshift(@docGenerationOptions, $runWithJhbuild);
+        }
+
+        if (system(@docGenerationOptions)) {
+            die "\n gtkdoc did not build without warnings\n";
+        }
+    }
+
+    return 0;
+}
+
+sub jhbuildWrapperPrefixIfNeeded()
+{
+    if (-e getJhbuildPath()) {
+        if (isEfl()) {
+            return File::Spec->catfile(sourceDir(), "Tools", "efl", "run-with-jhbuild");
+        } elsif (isGtk()) {
+            return File::Spec->catfile(sourceDir(), "Tools", "gtk", "run-with-jhbuild");
+        }
+    }
+
+    return "";
+}
+
+sub removeCMakeCache()
+{
+    my $cacheFilePath = File::Spec->catdir(baseProductDir(), configuration(), "CMakeCache.txt");
+    unlink($cacheFilePath) if -e $cacheFilePath;
+}
+
+sub generateBuildSystemFromCMakeProject
+{
+    my ($port, $prefixPath, @cmakeArgs, $additionalCMakeArgs) = @_;
+    my $config = configuration();
+    my $buildPath = File::Spec->catdir(baseProductDir(), $config);
+    File::Path::mkpath($buildPath) unless -d $buildPath;
+    my $originalWorkingDirectory = getcwd();
+    chdir($buildPath) or die;
+
+    my @args;
+    push @args, "-DPORT=\"$port\"";
+    push @args, "-DCMAKE_INSTALL_PREFIX=\"$prefixPath\"" if $prefixPath;
+    push @args, "-DSHARED_CORE=ON" if isEfl() && $ENV{"ENABLE_DRT"};
+    if ($config =~ /release/i) {
+        push @args, "-DCMAKE_BUILD_TYPE=Release";
+    } elsif ($config =~ /debug/i) {
+        push @args, "-DCMAKE_BUILD_TYPE=Debug";
+    }
+    push @args, @cmakeArgs if @cmakeArgs;
+    push @args, $additionalCMakeArgs if $additionalCMakeArgs;
+
+    push @args, '"' . sourceDir() . '"';
+
+    # Compiler options to keep floating point values consistent
+    # between 32-bit and 64-bit architectures.
+    determineArchitecture();
+    if ($architecture ne "x86_64" && !isARM()) {
+        $ENV{'CXXFLAGS'} = "-march=pentium4 -msse2 -mfpmath=sse " . ($ENV{'CXXFLAGS'} || "");
+    }
+
+    # We call system("cmake @args") instead of system("cmake", @args) so that @args is
+    # parsed for shell metacharacters.
+    my $wrapper = jhbuildWrapperPrefixIfNeeded() . " ";
+    my $returnCode = system($wrapper . "cmake @args");
+
+    chdir($originalWorkingDirectory);
+    return $returnCode;
+}
+
+sub buildCMakeGeneratedProject($)
+{
+    my ($makeArgs) = @_;
+    my $config = configuration();
+    my $buildPath = File::Spec->catdir(baseProductDir(), $config);
+    if (! -d $buildPath) {
+        die "Must call generateBuildSystemFromCMakeProject() before building CMake project.";
+    }
+    my @args = ("--build", $buildPath, "--config", $config);
+    push @args, ("--", $makeArgs) if $makeArgs;
+
+    # We call system("cmake @args") instead of system("cmake", @args) so that @args is
+    # parsed for shell metacharacters. In particular, $makeArgs may contain such metacharacters.
+    my $wrapper = jhbuildWrapperPrefixIfNeeded() . " ";
+    return system($wrapper . "cmake @args");
+}
+
+sub cleanCMakeGeneratedProject()
+{
+    my $config = configuration();
+    my $buildPath = File::Spec->catdir(baseProductDir(), $config);
+    if (-d $buildPath) {
+        return system("cmake", "--build", $buildPath, "--config", $config, "--target", "clean");
+    }
+    return 0;
+}
+
+sub buildCMakeProjectOrExit($$$$@)
+{
+    my ($clean, $port, $prefixPath, $makeArgs, @cmakeArgs) = @_;
+    my $returnCode;
+
+    exit(exitStatus(cleanCMakeGeneratedProject())) if $clean;
+
+    if (isEfl() && checkForArgumentAndRemoveFromARGV("--update-efl")) {
+        system("perl", "$sourceDir/Tools/Scripts/update-webkitefl-libs") == 0 or die $!;
+    }
+
+
+    $returnCode = exitStatus(generateBuildSystemFromCMakeProject($port, $prefixPath, @cmakeArgs));
+    exit($returnCode) if $returnCode;
+    if (isBlackBerry()) {
+        return 0 if (defined($ENV{"GENERATE_CMAKE_PROJECT_ONLY"}) eq '1');
+    }
+    $returnCode = exitStatus(buildCMakeGeneratedProject($makeArgs));
+    exit($returnCode) if $returnCode;
+    return 0;
+}
+
+sub cmakeBasedPortArguments()
+{
+    return blackberryCMakeArguments() if isBlackBerry();
+    return ('-DCMAKE_WINCE_SDK="STANDARDSDK_500 (ARMV4I)"') if isWinCE();
+    return ();
+}
+
+sub cmakeBasedPortName()
+{
+    return "BlackBerry" if isBlackBerry();
+    return "Efl" if isEfl();
+    return "WinCE" if isWinCE();
+    return "";
+}
+
+sub promptUser
+{
+    my ($prompt, $default) = @_;
+    my $defaultValue = $default ? "[$default]" : "";
+    print "$prompt $defaultValue: ";
+    chomp(my $input = <STDIN>);
+    return $input ? $input : $default;
+}
+
+sub buildQMakeProjects
+{
+    my ($projects, $clean, @buildParams) = @_;
+
+    my @buildArgs = ();
+    my $qconfigs = "";
+
+    my $make = qtMakeCommand($qmakebin);
+    my $makeargs = "";
+    my $command;
+    my $installHeaders;
+    my $installLibs;
+    for my $i (0 .. $#buildParams) {
+        my $opt = $buildParams[$i];
+        if ($opt =~ /^--qmake=(.*)/i ) {
+            $qmakebin = $1;
+        } elsif ($opt =~ /^--qmakearg=(.*)/i ) {
+            push @buildArgs, $1;
+        } elsif ($opt =~ /^--makeargs=(.*)/i ) {
+            $makeargs = $1;
+        } elsif ($opt =~ /^--install-headers=(.*)/i ) {
+            $installHeaders = $1;
+        } elsif ($opt =~ /^--install-libs=(.*)/i ) {
+            $installLibs = $1;
+        } else {
+            push @buildArgs, $opt;
+        }
+    }
+
+    # Automatically determine the number of CPUs for make only if this make argument haven't already been specified.
+    if ($make eq "make" && $makeargs !~ /-j\s*\d+/i && (!defined $ENV{"MAKEFLAGS"} || ($ENV{"MAKEFLAGS"} !~ /-j\s*\d+/i ))) {
+        $makeargs .= " -j" . numberOfCPUs();
+    }
+
+    $make = "$make $makeargs";
+    $make =~ s/\s+$//;
+
+    my $originalCwd = getcwd();
+    my $dir = File::Spec->canonpath(productDir());
+    File::Path::mkpath($dir);
+    chdir $dir or die "Failed to cd into " . $dir . "\n";
+
+    if ($clean) {
+        $command = "$make distclean";
+        print "\nCalling '$command' in " . $dir . "\n\n";
+        return system $command;
+    }
+
+    my $qmakepath = File::Spec->catfile(sourceDir(), "Tools", "qmake");
+    my $qmakecommand = $qmakebin;
+
+    my $config = configuration();
+    push @buildArgs, "INSTALL_HEADERS=" . $installHeaders if defined($installHeaders);
+    push @buildArgs, "INSTALL_LIBS=" . $installLibs if defined($installLibs);
+
+    my $passedConfig = passedConfiguration() || "";
+    if ($passedConfig =~ m/debug/i) {
+        push @buildArgs, "CONFIG-=release";
+        push @buildArgs, "CONFIG+=debug";
+    } elsif ($passedConfig =~ m/release/i) {
+        push @buildArgs, "CONFIG+=release";
+        push @buildArgs, "CONFIG-=debug";
+    } elsif ($passedConfig) {
+        die "Build type $passedConfig is not supported with --qt.\n";
+    }
+
+    # Using build-webkit to build assumes you want a developer-build
+    push @buildArgs, "CONFIG-=production_build";
+
+    my $svnRevision = currentSVNRevision();
+    my $previousSvnRevision = "unknown";
+
+    my $buildHint = "";
+
+    my $pathToQmakeCache = File::Spec->catfile($dir, ".qmake.cache");
+    if (-e $pathToQmakeCache && open(QMAKECACHE, $pathToQmakeCache)) {
+        while (<QMAKECACHE>) {
+            if ($_ =~ m/^SVN_REVISION\s=\s(\d+)$/) {
+                $previousSvnRevision = $1;
+            }
+        }
+        close(QMAKECACHE);
+    }
+
+    my $result = 0;
+
+    # Run qmake, regadless of having a makefile or not, so that qmake can
+    # detect changes to the configuration.
+
+    push @buildArgs, "-after OVERRIDE_SUBDIRS=\"@{$projects}\"" if @{$projects};
+    unshift @buildArgs, File::Spec->catfile(sourceDir(), "WebKit.pro");
+    $command = "$qmakecommand @buildArgs";
+    print "Calling '$command' in " . $dir . "\n\n";
+    print "Installation headers directory: $installHeaders\n" if(defined($installHeaders));
+    print "Installation libraries directory: $installLibs\n" if(defined($installLibs));
+
+    my $configChanged = 0;
+    open(QMAKE, "$command 2>&1 |") || die "Could not execute qmake";
+    while (<QMAKE>) {
+        $configChanged = 1 if $_ =~ m/The configuration was changed since the last build/;
+        print $_;
+    }
+
+    close(QMAKE);
+    $result = $?;
+
+    if ($result ne 0) {
+       die "\nFailed to set up build environment using $qmakebin!\n";
+    }
+
+    my $maybeNeedsCleanBuild = 0;
+    my $needsIncrementalBuild = 0;
+
+    if ($svnRevision ne $previousSvnRevision) {
+        print "Last built revision was " . $previousSvnRevision .
+            ", now at revision $svnRevision. Full incremental build needed.\n";
+        $needsIncrementalBuild = 1;
+
+        my @fileList = listOfChangedFilesBetweenRevisions(sourceDir(), $previousSvnRevision, $svnRevision);
+
+        foreach (@fileList) {
+            if (m/\.pr[oif]$/ or
+                m/\.qmake.conf$/ or
+                m/^Tools\/qmake\//
+               ) {
+                print "Change to $_ detected, clean build may be needed.\n";
+                $maybeNeedsCleanBuild = 1;
+                last;
+            }
+        }
+    }
+
+    if ($configChanged) {
+        print "Calling '$make wipeclean' in " . $dir . "\n\n";
+        $result = system "$make wipeclean";
+    }
+
+    $command = "$make";
+    if ($needsIncrementalBuild) {
+        $command .= " incremental";
+    }
+
+    print "\nCalling '$command' in " . $dir . "\n\n";
+    $result = system $command;
+
+    chdir ".." or die;
+
+    if ($result eq 0) {
+        # Now that the build completed successfully we can save the SVN revision
+        open(QMAKECACHE, ">>$pathToQmakeCache");
+        print QMAKECACHE "SVN_REVISION = $svnRevision\n";
+        close(QMAKECACHE);
+    } elsif (!$command =~ /incremental/ && exitStatus($result)) {
+        my $exitCode = exitStatus($result);
+        my $failMessage = <<EOF;
+
+===== BUILD FAILED ======
+
+The build failed with exit code $exitCode. This may have been because you
+
+  - added an #include to a source/header
+  - added a Q_OBJECT macro to a class
+  - added a new resource to a qrc file
+
+as dependencies are not automatically re-computed for local developer builds.
+You may try computing dependencies manually by running 'make qmake_all' in:
+
+  $dir
+
+or passing --makeargs="qmake_all" to build-webkit.
+
+=========================
+
+EOF
+        print "$failMessage";
+    } elsif ($maybeNeedsCleanBuild) {
+        print "\nIncremental build failed, clean build needed. \n";
+        print "Calling '$make wipeclean' in " . $dir . "\n\n";
+        chdir $dir or die;
+        system "$make wipeclean";
+
+        print "\nCalling '$make' in " . $dir . "\n\n";
+        $result = system $make;
+    }
+
+    return $result;
+}
+
+sub buildGtkProject
+{
+    my ($project, $clean, @buildArgs) = @_;
+
+    if ($project ne "WebKit" and $project ne "JavaScriptCore" and $project ne "WTF") {
+        die "Unsupported project: $project. Supported projects: WebKit, JavaScriptCore, WTF\n";
+    }
+
+    return buildAutotoolsProject($project, $clean, @buildArgs);
+}
+
+sub buildChromiumMakefile($$@)
+{
+    my ($target, $clean, @options) = @_;
+    if ($clean) {
+        return system qw(rm -rf out);
+    }
+    my $config = configuration();
+    my $numCpus = numberOfCPUs();
+    my $makeArgs;
+    for (@options) {
+        $makeArgs = $1 if /^--makeargs=(.*)/i;
+    }
+    $makeArgs = "-j$numCpus" if not $makeArgs;
+    my $command .= "make -fMakefile.chromium $makeArgs BUILDTYPE=$config $target";
+
+    print "$command\n";
+    return system $command;
+}
+
+sub buildChromiumNinja($$@)
+{
+    # rm -rf out requires rerunning gyp, so don't support --clean for now.
+    my ($target, @options) = @_;
+    my $config = configuration();
+    my $makeArgs = "";
+    for (@options) {
+        $makeArgs = $1 if /^--makeargs=(.*)/i;
+    }
+    my $command = "";
+
+    # Find ninja.
+    my $ninjaPath;
+    if (commandExists('ninja')) {
+        $ninjaPath = 'ninja';
+    } elsif (-e 'Source/WebKit/chromium/depot_tools/ninja') {
+        $ninjaPath = 'Source/WebKit/chromium/depot_tools/ninja';
+    } else {
+        die "ninja not found. Install chromium's depot_tools by running update-webkit first\n";
+    }
+
+    $command .= "$ninjaPath -C out/$config $target $makeArgs";
+
+    print "$command\n";
+    return system $command;
+}
+
+sub buildChromiumVisualStudioProject($$)
+{
+    my ($projectPath, $clean) = @_;
+
+    my $config = configuration();
+    my $action = "/build";
+    $action = "/clean" if $clean;
+
+    # Find Visual Studio installation.
+    my $vsInstallDir;
+    my $programFilesPath = $ENV{'PROGRAMFILES'} || "C:\\Program Files";
+    if ($ENV{'VSINSTALLDIR'}) {
+        $vsInstallDir = $ENV{'VSINSTALLDIR'};
+    } else {
+        $vsInstallDir = "$programFilesPath/Microsoft Visual Studio 8";
+    }
+    $vsInstallDir =~ s,\\,/,g;
+    $vsInstallDir = `cygpath "$vsInstallDir"` if isCygwin();
+    chomp $vsInstallDir;
+    $vcBuildPath = "$vsInstallDir/Common7/IDE/devenv.com";
+    if (! -e $vcBuildPath) {
+        # Visual Studio not found, try VC++ Express
+        $vcBuildPath = "$vsInstallDir/Common7/IDE/VCExpress.exe";
+        if (! -e $vcBuildPath) {
+            print "*************************************************************\n";
+            print "Cannot find '$vcBuildPath'\n";
+            print "Please execute the file 'vcvars32.bat' from\n";
+            print "'$programFilesPath\\Microsoft Visual Studio 8\\VC\\bin\\'\n";
+            print "to setup the necessary environment variables.\n";
+            print "*************************************************************\n";
+            die;
+        }
+    }
+
+    # Create command line and execute it.
+    my @command = ($vcBuildPath, $projectPath, $action, $config);
+    print "Building results into: ", baseProductDir(), "\n";
+    print join(" ", @command), "\n";
+    return system @command;
+}
+
+sub buildChromium($@)
+{
+    my ($clean, @options) = @_;
+
+    # We might need to update DEPS or re-run GYP if things have changed.
+    if (checkForArgumentAndRemoveFromArrayRef("--update-chromium", \@options)) {
+        my @updateCommand = ("perl", "Tools/Scripts/update-webkit-chromium", "--force");
+        push @updateCommand, "--chromium-android" if isChromiumAndroid();
+        system(@updateCommand) == 0 or die $!;
+    }
+
+    my $result = 1;
+    if (isDarwin() && !isChromiumAndroid() && !isChromiumMacMake() && !isChromiumNinja()) {
+        # Mac build - builds the root xcode project.
+        $result = buildXCodeProject("Source/WebKit/chromium/All", $clean, "-configuration", configuration(), @options);
+    } elsif (isCygwin() || isWindows()) {
+        # Windows build - builds the root visual studio solution.
+        $result = buildChromiumVisualStudioProject("Source/WebKit/chromium/All.sln", $clean);
+    } elsif (isChromiumNinja() && !isChromiumAndroid()) {
+        $result = buildChromiumNinja("all", $clean, @options);
+    } elsif (isLinux() || isChromiumAndroid() || isChromiumMacMake()) {
+        # Linux build - build using make.
+        $result = buildChromiumMakefile("all", $clean, @options);
+    } else {
+        print STDERR "This platform is not supported by chromium.\n";
+    }
+    return $result;
+}
+
+sub appleApplicationSupportPath
+{
+    open INSTALL_DIR, "</proc/registry/HKEY_LOCAL_MACHINE/SOFTWARE/Apple\ Inc./Apple\ Application\ Support/InstallDir";
+    my $path = <INSTALL_DIR>;
+    $path =~ s/[\r\n\x00].*//;
+    close INSTALL_DIR;
+
+    my $unixPath = `cygpath -u '$path'`;
+    chomp $unixPath;
+    return $unixPath;
+}
+
+sub setPathForRunningWebKitApp
+{
+    my ($env) = @_;
+
+    if (isAppleWinWebKit()) {
+        $env->{PATH} = join(':', productDir(), dirname(installedSafariPath()), appleApplicationSupportPath(), $env->{PATH} || "");
+    } elsif (isQt()) {
+        my $qtLibs = `$qmakebin -query QT_INSTALL_LIBS`;
+        $qtLibs =~ s/[\n|\r]$//g;
+        $env->{PATH} = join(';', $qtLibs, productDir() . "/lib", $env->{PATH} || "");
+    }
+}
+
+sub printHelpAndExitForRunAndDebugWebKitAppIfNeeded
+{
+    return unless checkForArgumentAndRemoveFromARGV("--help");
+
+    my ($includeOptionsForDebugging) = @_;
+
+    print STDERR <<EOF;
+Usage: @{[basename($0)]} [options] [args ...]
+  --help                            Show this help message
+  --no-saved-state                  Launch the application without state restoration (OS X 10.7 and later)
+  --guard-malloc                    Enable Guard Malloc (OS X only)
+  --use-web-process-xpc-service     Launch the Web Process as an XPC Service (OS X only)
+EOF
+
+    if ($includeOptionsForDebugging) {
+        print STDERR <<EOF;
+  --target-web-process              Debug the web process
+  --use-gdb                         Use GDB (this is the default when using Xcode 4.4 or earlier)
+  --use-lldb                        Use LLDB (this is the default when using Xcode 4.5 or later)
+EOF
+    }
+
+    exit(1);
+}
+
+sub argumentsForRunAndDebugMacWebKitApp()
+{
+    my @args = ();
+    push @args, ("-ApplePersistenceIgnoreState", "YES") if !isSnowLeopard() && checkForArgumentAndRemoveFromArrayRef("--no-saved-state", \@args);
+    push @args, ("-WebKit2UseXPCServiceForWebProcess", "YES") if shouldUseXPCServiceForWebProcess();
+    unshift @args, @ARGV;
+
+    return @args;
+}
+
+sub runMacWebKitApp($;$)
+{
+    my ($appPath, $useOpenCommand) = @_;
+    my $productDir = productDir();
+    print "Starting @{[basename($appPath)]} with DYLD_FRAMEWORK_PATH set to point to built WebKit in $productDir.\n";
+    $ENV{DYLD_FRAMEWORK_PATH} = $productDir;
+    $ENV{WEBKIT_UNSET_DYLD_FRAMEWORK_PATH} = "YES";
+
+    setUpGuardMallocIfNeeded();
+
+    if (defined($useOpenCommand) && $useOpenCommand == USE_OPEN_COMMAND) {
+        return system("open", "-W", "-a", $appPath, "--args", argumentsForRunAndDebugMacWebKitApp());
+    }
+    if (architecture()) {
+        return system "arch", "-" . architecture(), $appPath, argumentsForRunAndDebugMacWebKitApp();
+    }
+    return system { $appPath } $appPath, argumentsForRunAndDebugMacWebKitApp();
+}
+
+sub execMacWebKitAppForDebugging($)
+{
+    my ($appPath) = @_;
+    my $architectureSwitch;
+    my $argumentsSeparator;
+
+    if (debugger() eq "lldb") {
+        $architectureSwitch = "--arch";
+        $argumentsSeparator = "--";
+    } elsif (debugger() eq "gdb") {
+        $architectureSwitch = "-arch";
+        $argumentsSeparator = "--args";
+    } else {
+        die "Unknown debugger $debugger.\n";
+    }
+
+    my $debuggerPath = `xcrun -find $debugger`;
+    chomp $debuggerPath;
+    die "Can't find the $debugger executable.\n" unless -x $debuggerPath;
+
+    my $productDir = productDir();
+    $ENV{DYLD_FRAMEWORK_PATH} = $productDir;
+    $ENV{WEBKIT_UNSET_DYLD_FRAMEWORK_PATH} = "YES";
+
+    setUpGuardMallocIfNeeded();
+
+    my @architectureFlags = ($architectureSwitch, architecture());
+    if (!shouldTargetWebProcess()) {
+        print "Starting @{[basename($appPath)]} under $debugger with DYLD_FRAMEWORK_PATH set to point to built WebKit in $productDir.\n";
+        exec { $debuggerPath } $debuggerPath, @architectureFlags, $argumentsSeparator, $appPath, argumentsForRunAndDebugMacWebKitApp() or die;
+    } else {
+        if (shouldUseXPCServiceForWebProcess()) {
+            die "Targetting the Web Process is not compatible with using an XPC Service for the Web Process at this time.";
+        }
+        
+        my $webProcessShimPath = File::Spec->catfile($productDir, "WebProcessShim.dylib");
+        my $webProcessPath = File::Spec->catdir($productDir, "WebProcess.app");
+        my $webKit2ExecutablePath = File::Spec->catfile($productDir, "WebKit2.framework", "WebKit2");
+
+        appendToEnvironmentVariableList("DYLD_INSERT_LIBRARIES", $webProcessShimPath);
+
+        print "Starting WebProcess under $debugger with DYLD_FRAMEWORK_PATH set to point to built WebKit in $productDir.\n";
+        exec { $debuggerPath } $debuggerPath, @architectureFlags, $argumentsSeparator, $webProcessPath, $webKit2ExecutablePath, "-type", "webprocess", "-client-executable", $appPath or die;
+    }
+}
+
+sub debugSafari
+{
+    if (isAppleMacWebKit()) {
+        checkFrameworks();
+        execMacWebKitAppForDebugging(safariPath());
+    }
+
+    if (isAppleWinWebKit()) {
+        setupCygwinEnv();
+        my $productDir = productDir();
+        chomp($ENV{WEBKITNIGHTLY} = `cygpath -wa "$productDir"`);
+        my $safariPath = safariPath();
+        chomp($safariPath = `cygpath -wa "$safariPath"`);
+        return system { $vcBuildPath } $vcBuildPath, "/debugexe", "\"$safariPath\"", @ARGV;
+    }
+
+    return 1; # Unsupported platform; can't debug Safari on this platform.
+}
+
+sub runSafari
+{
+
+    if (isAppleMacWebKit()) {
+        return runMacWebKitApp(safariPath());
+    }
+
+    if (isAppleWinWebKit()) {
+        my $result;
+        my $productDir = productDir();
+        my $webKitLauncherPath = File::Spec->catfile(productDir(), "WebKit.exe");
+        return system { $webKitLauncherPath } $webKitLauncherPath, @ARGV;
+    }
+
+    return 1; # Unsupported platform; can't run Safari on this platform.
+}
+
+sub runMiniBrowser
+{
+    if (isAppleMacWebKit()) {
+        return runMacWebKitApp(File::Spec->catfile(productDir(), "MiniBrowser.app", "Contents", "MacOS", "MiniBrowser"));
+    }
+
+    return 1;
+}
+
+sub debugMiniBrowser
+{
+    if (isAppleMacWebKit()) {
+        execMacWebKitAppForDebugging(File::Spec->catfile(productDir(), "MiniBrowser.app", "Contents", "MacOS", "MiniBrowser"));
+    }
+    
+    return 1;
+}
+
+sub runWebKitTestRunner
+{
+    if (isAppleMacWebKit()) {
+        return runMacWebKitApp(File::Spec->catfile(productDir(), "WebKitTestRunner"));
+    } elsif (isGtk()) {
+        my $productDir = productDir();
+        my $injectedBundlePath = "$productDir/Libraries/.libs/libTestRunnerInjectedBundle";
+        print "Starting WebKitTestRunner with TEST_RUNNER_INJECTED_BUNDLE_FILENAME set to point to $injectedBundlePath.\n";
+        $ENV{TEST_RUNNER_INJECTED_BUNDLE_FILENAME} = $injectedBundlePath;
+        my @args = ("$productDir/Programs/WebKitTestRunner", @ARGV);
+        return system {$args[0] } @args;
+    }
+
+    return 1;
+}
+
+sub debugWebKitTestRunner
+{
+    if (isAppleMacWebKit()) {
+        execMacWebKitAppForDebugging(File::Spec->catfile(productDir(), "WebKitTestRunner"));
+    }
+
+    return 1;
+}
+
+sub runTestWebKitAPI
+{
+    if (isAppleMacWebKit()) {
+        return runMacWebKitApp(File::Spec->catfile(productDir(), "TestWebKitAPI"));
+    }
+
+    return 1;
+}
+
+sub readRegistryString
+{
+    my ($valueName) = @_;
+    chomp(my $string = `regtool --wow32 get "$valueName"`);
+    return $string;
+}
+
+sub writeRegistryString
+{
+    my ($valueName, $string) = @_;
+
+    my $error = system "regtool", "--wow32", "set", "-s", $valueName, $string;
+
+    # On Windows Vista/7 with UAC enabled, regtool will fail to modify the registry, but will still
+    # return a successful exit code. So we double-check here that the value we tried to write to the
+    # registry was really written.
+    return !$error && readRegistryString($valueName) eq $string;
+}
+
+1;
diff --git a/Tools/Scripts/webkitperl/FeatureList.pm b/Tools/Scripts/webkitperl/FeatureList.pm
new file mode 100644
index 0000000..cc5f0de
--- /dev/null
+++ b/Tools/Scripts/webkitperl/FeatureList.pm
@@ -0,0 +1,447 @@
+# Copyright (C) 2012 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:
+#
+# 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 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 to contain all the enable/disable feature option code.
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+BEGIN {
+   use Exporter   ();
+   our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
+   $VERSION     = 1.00;
+   @ISA         = qw(Exporter);
+   @EXPORT      = qw(&getFeatureOptionList);
+   %EXPORT_TAGS = ( );
+   @EXPORT_OK   = ();
+}
+
+my (
+    $threeDRenderingSupport,
+    $accelerated2DCanvasSupport,
+    $animationAPISupport,
+    $batteryStatusSupport,
+    $blobSupport,
+    $channelMessagingSupport,
+    $cspNextSupport,
+    $css3ConditionalRulesSupport,
+    $css3TextSupport,
+    $cssBoxDecorationBreakSupport,
+    $cssDeviceAdaptation,
+    $cssExclusionsSupport,
+    $cssFiltersSupport,
+    $cssHierarchiesSupport,
+    $cssImageOrientationSupport,
+    $cssImageResolutionSupport,
+    $cssRegionsSupport,
+    $cssShadersSupport,
+    $cssCompositingSupport,
+    $cssVariablesSupport,
+    $customSchemeHandlerSupport,
+    $dataTransferItemsSupport,
+    $datalistSupport,
+    $detailsSupport,
+    $deviceOrientationSupport,
+    $dialogElementSupport,
+    $directoryUploadSupport,
+    $downloadAttributeSupport,
+    $fileSystemSupport,
+    $filtersSupport,
+    $ftpDirSupport,
+    $fullscreenAPISupport,
+    $gamepadSupport,
+    $geolocationSupport,
+    $highDPICanvasSupport,
+    $icondatabaseSupport,
+    $iframeSeamlessSupport,
+    $imageResizerSupport,
+    $indexedDatabaseSupport,
+    $inputSpeechSupport,
+    $inputTypeColorSupport,
+    $inputTypeDateSupport,
+    $inputTypeDatetimeSupport,
+    $inputTypeDatetimelocalSupport,
+    $inputTypeMonthSupport,
+    $inputTypeTimeSupport,
+    $inputTypeWeekSupport,
+    $inspectorSupport,
+    $javascriptDebuggerSupport,
+    $legacyNotificationsSupport,
+    $legacyVendorPrefixSupport,
+    $legacyWebAudioSupport,
+    $linkPrefetchSupport,
+    $linkPrerenderSupport,
+    $mathmlSupport,
+    $mediaCaptureSupport,
+    $mediaSourceSupport,
+    $mediaStatisticsSupport,
+    $mediaStreamSupport,
+    $meterTagSupport,
+    $mhtmlSupport,
+    $microdataSupport,
+    $mutationObserversSupport,
+    $netscapePluginAPISupport,
+    $networkInfoSupport,
+    $notificationsSupport,
+    $orientationEventsSupport,
+    $pageVisibilityAPISupport,
+    $progressTagSupport,
+    $quotaSupport,
+    $resolutionMediaQuerySupport,
+    $registerProtocolHandlerSupport,
+    $requestAnimationFrameSupport,
+    $scriptedSpeechSupport,
+    $shadowDOMSupport,
+    $sharedWorkersSupport,
+    $sqlDatabaseSupport,
+    $styleScopedSupport,
+    $svgDOMObjCBindingsSupport,
+    $svgFontsSupport,
+    $svgSupport,
+    $systemMallocSupport,
+    $textAutosizingSupport,
+    $tiledBackingStoreSupport,
+    $touchEventsSupport,
+    $touchIconLoadingSupport,
+    $vibrationSupport,
+    $videoSupport,
+    $videoTrackSupport,
+    $webglSupport,
+    $webAudioSupport,
+    $webIntentsSupport,
+    $webIntentsTagSupport,
+    $webSocketsSupport,
+    $webTimingSupport,
+    $workersSupport,
+    $xhrResponseBlobSupport,
+    $xhrTimeoutSupport,
+    $xsltSupport,
+);
+
+my @features = (
+    { option => "3d-rendering", desc => "Toggle 3D Rendering support",
+      define => "ENABLE_3D_RENDERING", default => (isAppleMacWebKit() || isQt()), value => \$threeDRenderingSupport },
+
+    { option => "accelerated-2d-canvas", desc => "Toggle Accelerated 2D Canvas support",
+      define => "ENABLE_ACCELERATED_2D_CANVAS", default => 0, value => \$accelerated2DCanvasSupport },
+
+    { option => "animation-api", desc => "Toggle Animation API support",
+      define => "ENABLE_ANIMATION_API", default => (isBlackBerry() || isEfl()), value => \$animationAPISupport },
+
+    { option => "battery-status", desc => "Toggle Battery Status support",
+      define => "ENABLE_BATTERY_STATUS", default => (isEfl() || isBlackBerry()), value => \$batteryStatusSupport },
+
+    { option => "blob", desc => "Toggle Blob support",
+      define => "ENABLE_BLOB", default => (isAppleMacWebKit() || isGtk() || isChromium() || isBlackBerry() || isEfl()), value => \$blobSupport },
+
+    { option => "channel-messaging", desc => "Toggle Channel Messaging support",
+      define => "ENABLE_CHANNEL_MESSAGING", default => 1, value => \$channelMessagingSupport },
+
+    { option => "csp-next", desc => "Toggle Content Security Policy 1.1 support",
+      define => "ENABLE_CSP_NEXT", default => 0, value => \$cspNextSupport },
+
+    { option => "css-device-adaptation", desc => "Toggle CSS Device Adaptation support",
+      define => "ENABLE_CSS_DEVICE_ADAPTATION", default => isEfl(), value => \$cssDeviceAdaptation },
+
+    { option => "css-exclusions", desc => "Toggle CSS Exclusions support",
+      define => "ENABLE_CSS_EXCLUSIONS", default => 1, value => \$cssExclusionsSupport },
+
+    { option => "css-filters", desc => "Toggle CSS Filters support",
+      define => "ENABLE_CSS_FILTERS", default => isAppleWebKit() || isBlackBerry(), value => \$cssFiltersSupport },
+
+    { option => "css3-conditional-rules", desc => "Toggle CSS3 Conditional Rules support (i.e. \@supports)",
+      define => "ENABLE_CSS3_CONDITIONAL_RULES", default => 0, value => \$css3ConditionalRulesSupport },
+
+    { option => "css3-text", desc => "Toggle CSS3 Text support",
+      define => "ENABLE_CSS3_TEXT", default => isEfl(), value => \$css3TextSupport },
+
+    { option => "css-hierarchies", desc => "Toggle CSS Hierarchy support",
+      define => "ENABLE_CSS_HIERARCHIES", default => 0, value => \$cssHierarchiesSupport },
+
+    { option => "css-box-decoration-break", desc => "Toggle CSS box-decoration-break support",
+      define => "ENABLE_CSS_BOX_DECORATION_BREAK", default => 1, value => \$cssBoxDecorationBreakSupport },
+
+    { option => "css-image-orientation", desc => "Toggle CSS image-orientation support",
+      define => "ENABLE_CSS_IMAGE_ORIENTATION", default => 0, value => \$cssImageOrientationSupport },
+
+    { option => "css-image-resolution", desc => "Toggle CSS image-resolution support",
+      define => "ENABLE_CSS_IMAGE_RESOLUTION", default => 0, value => \$cssImageResolutionSupport },
+
+    { option => "css-regions", desc => "Toggle CSS Regions support",
+      define => "ENABLE_CSS_REGIONS", default => 1, value => \$cssRegionsSupport },
+
+    { option => "css-shaders", desc => "Toggle CSS Shaders support",
+      define => "ENABLE_CSS_SHADERS", default => isAppleMacWebKit(), value => \$cssShadersSupport },
+
+    { option => "css-compositing", desc => "Toggle CSS Compositing support",
+      define => "ENABLE_CSS_COMPOSITING", default => isAppleWebKit(), value => \$cssCompositingSupport },
+
+    { option => "css-variables", desc => "Toggle CSS Variable support",
+      define => "ENABLE_CSS_VARIABLES", default => (isBlackBerry() || isEfl()), value => \$cssVariablesSupport },
+
+    { option => "custom-scheme-handler", desc => "Toggle Custom Scheme Handler support",
+      define => "ENABLE_CUSTOM_SCHEME_HANDLER", default => (isBlackBerry() || isEfl()), value => \$customSchemeHandlerSupport },
+
+    { option => "datalist", desc => "Toggle Datalist support",
+      define => "ENABLE_DATALIST_ELEMENT", default => isEfl(), value => \$datalistSupport },
+
+    { option => "data-transfer-items", desc => "Toggle Data Transfer Items support",
+      define => "ENABLE_DATA_TRANSFER_ITEMS", default => 0, value => \$dataTransferItemsSupport },
+
+    { option => "details", desc => "Toggle Details support",
+      define => "ENABLE_DETAILS_ELEMENT", default => 1, value => \$detailsSupport },
+
+    { option => "device-orientation", desc => "Toggle Device Orientation support",
+      define => "ENABLE_DEVICE_ORIENTATION", default => isBlackBerry(), value => \$deviceOrientationSupport },
+
+    { option => "dialog", desc => "Toggle Dialog Element support",
+      define => "ENABLE_DIALOG_ELEMENT", default => 0, value => \$dialogElementSupport },
+
+    { option => "directory-upload", desc => "Toggle Directory Upload support",
+      define => "ENABLE_DIRECTORY_UPLOAD", default => 0, value => \$directoryUploadSupport },
+
+    { option => "download-attribute", desc => "Toggle Download Attribute support",
+      define => "ENABLE_DOWNLOAD_ATTRIBUTE", default => (isBlackBerry() || isEfl()), value => \$downloadAttributeSupport },
+
+    { option => "file-system", desc => "Toggle File System support",
+      define => "ENABLE_FILE_SYSTEM", default => isBlackBerry(), value => \$fileSystemSupport },
+
+    { option => "filters", desc => "Toggle Filters support",
+      define => "ENABLE_FILTERS", default => (isAppleWebKit() || isGtk() || isQt() || isEfl() || isBlackBerry()), value => \$filtersSupport },
+
+    { option => "ftpdir", desc => "Toggle FTP Directory support",
+      define => "ENABLE_FTPDIR", default => !isWinCE(), value => \$ftpDirSupport },
+
+    { option => "fullscreen-api", desc => "Toggle Fullscreen API support",
+      define => "ENABLE_FULLSCREEN_API", default => (isAppleMacWebKit() || isEfl() || isGtk() || isBlackBerry() || isQt()), value => \$fullscreenAPISupport },
+
+    { option => "gamepad", desc => "Toggle Gamepad support",
+      define => "ENABLE_GAMEPAD", default => (isEfl() || isGtk() || isQt()), value => \$gamepadSupport },
+
+    { option => "geolocation", desc => "Toggle Geolocation support",
+      define => "ENABLE_GEOLOCATION", default => (isAppleWebKit() || isGtk() || isBlackBerry()), value => \$geolocationSupport },
+
+    { option => "high-dpi-canvas", desc => "Toggle High DPI Canvas support",
+      define => "ENABLE_HIGH_DPI_CANVAS", default => (isAppleWebKit()), value => \$highDPICanvasSupport },
+
+    { option => "icon-database", desc => "Toggle Icondatabase support",
+      define => "ENABLE_ICONDATABASE", default => 1, value => \$icondatabaseSupport },
+
+    { option => "iframe-seamless", desc => "Toggle iframe seamless attribute support",
+      define => "ENABLE_IFRAME_SEAMLESS", default => 1, value => \$iframeSeamlessSupport },
+
+    { option => "indexed-database", desc => "Toggle Indexed Database support",
+      define => "ENABLE_INDEXED_DATABASE", default => 0, value => \$indexedDatabaseSupport },
+
+    { option => "input-speech", desc => "Toggle Input Speech support",
+      define => "ENABLE_INPUT_SPEECH", default => 0, value => \$inputSpeechSupport },
+
+    { option => "input-type-color", desc => "Toggle Input Type Color support",
+      define => "ENABLE_INPUT_TYPE_COLOR", default => (isBlackBerry() || isEfl() || isQt()), value => \$inputTypeColorSupport },
+
+    { option => "input-type-date", desc => "Toggle Input Type Date support",
+      define => "ENABLE_INPUT_TYPE_DATE", default => 0, value => \$inputTypeDateSupport },
+
+    { option => "input-type-datetime", desc => "Toggle Input Type Datetime support",
+      define => "ENABLE_INPUT_TYPE_DATETIME", default => 0, value => \$inputTypeDatetimeSupport },
+
+    { option => "input-type-datetimelocal", desc => "Toggle Input Type Datetimelocal support",
+      define => "ENABLE_INPUT_TYPE_DATETIMELOCAL", default => 0, value => \$inputTypeDatetimelocalSupport },
+
+    { option => "input-type-month", desc => "Toggle Input Type Month support",
+      define => "ENABLE_INPUT_TYPE_MONTH", default => 0, value => \$inputTypeMonthSupport },
+
+    { option => "input-type-time", desc => "Toggle Input Type Time support",
+      define => "ENABLE_INPUT_TYPE_TIME", default => 0, value => \$inputTypeTimeSupport },
+
+    { option => "input-type-week", desc => "Toggle Input Type Week support",
+      define => "ENABLE_INPUT_TYPE_WEEK", default => 0, value => \$inputTypeWeekSupport },
+
+    { option => "inspector", desc => "Toggle Inspector support",
+      define => "ENABLE_INSPECTOR", default => !isWinCE(), value => \$inspectorSupport },
+
+    { option => "javascript-debugger", desc => "Toggle JavaScript Debugger support",
+      define => "ENABLE_JAVASCRIPT_DEBUGGER", default => 1, value => \$javascriptDebuggerSupport },
+
+    { option => "legacy-notifications", desc => "Toggle Legacy Notifications support",
+      define => "ENABLE_LEGACY_NOTIFICATIONS", default => isBlackBerry(), value => \$legacyNotificationsSupport },
+
+    { option => "legacy-vendor-prefixes", desc => "Toggle Legacy Vendor Prefix support",
+      define => "ENABLE_LEGACY_VENDOR_PREFIXES", default => !isChromium(), value => \$legacyVendorPrefixSupport },
+
+    { option => "legacy-web-audio", desc => "Toggle Legacy Web Audio support",
+      define => "ENABLE_LEGACY_WEB_AUDIO", default => 1, value => \$legacyWebAudioSupport },
+
+    { option => "link-prefetch", desc => "Toggle Link Prefetch support",
+      define => "ENABLE_LINK_PREFETCH", default => (isGtk() || isEfl()), value => \$linkPrefetchSupport },
+
+    { option => "link-prerender", desc => "Toggle Link Prerender support",
+      define => "ENABLE_LINK_PRERENDER", default => 0, value => \$linkPrerenderSupport },
+
+    { option => "mathml", desc => "Toggle MathML support",
+      define => "ENABLE_MATHML", default => 1, value => \$mathmlSupport },
+
+    { option => "media-capture", desc => "Toggle Media Capture support",
+      define => "ENABLE_MEDIA_CAPTURE", default => isEfl(), value => \$mediaCaptureSupport },
+
+    { option => "media-source", desc => "Toggle Media Source support",
+      define => "ENABLE_MEDIA_SOURCE", default => 0, value => \$mediaSourceSupport },
+
+    { option => "media-statistics", desc => "Toggle Media Statistics support",
+      define => "ENABLE_MEDIA_STATISTICS", default => 0, value => \$mediaStatisticsSupport },
+
+    { option => "media-stream", desc => "Toggle Media Stream support",
+      define => "ENABLE_MEDIA_STREAM", default => (isChromium() || isGtk() || isBlackBerry()), value => \$mediaStreamSupport },
+
+    { option => "meter-tag", desc => "Toggle Meter Tag support",
+      define => "ENABLE_METER_ELEMENT", default => !isAppleWinWebKit(), value => \$meterTagSupport },
+
+    { option => "mhtml", desc => "Toggle MHTML support",
+      define => "ENABLE_MHTML", default => isGtk(), value => \$mhtmlSupport },
+
+    { option => "microdata", desc => "Toggle Microdata support",
+      define => "ENABLE_MICRODATA", default => (isEfl() || isBlackBerry()), value => \$microdataSupport },
+
+    { option => "mutation-observers", desc => "Toggle Mutation Observers support",
+      define => "ENABLE_MUTATION_OBSERVERS", default => 1, value => \$mutationObserversSupport },
+
+    { option => "navigator-content-utils", desc => "Toggle Navigator Content Utils support",
+      define => "ENABLE_NAVIGATOR_CONTENT_UTILS", default => (isBlackBerry() || isEfl()), value => \$registerProtocolHandlerSupport },
+
+    { option => "netscape-plugin-api", desc => "Toggle Netscape Plugin API support",
+      define => "ENABLE_NETSCAPE_PLUGIN_API", default => 1, value => \$netscapePluginAPISupport },
+
+    { option => "network-info", desc => "Toggle Network Info support",
+      define => "ENABLE_NETWORK_INFO", default => (isEfl() || isBlackBerry()), value => \$networkInfoSupport },
+
+    { option => "notifications", desc => "Toggle Notifications support",
+      define => "ENABLE_NOTIFICATIONS", default => isBlackBerry(), value => \$notificationsSupport },
+
+    { option => "orientation-events", desc => "Toggle Orientation Events support",
+      define => "ENABLE_ORIENTATION_EVENTS", default => isBlackBerry(), value => \$orientationEventsSupport },
+
+    { option => "page-visibility-api", desc => "Toggle Page Visibility API support",
+      define => "ENABLE_PAGE_VISIBILITY_API", default => (isBlackBerry() || isEfl()), value => \$pageVisibilityAPISupport },
+
+    { option => "progress-tag", desc => "Toggle Progress Tag support",
+      define => "ENABLE_PROGRESS_ELEMENT", default => 1, value => \$progressTagSupport },
+
+    { option => "quota", desc => "Toggle Quota support",
+      define => "ENABLE_QUOTA", default => 0, value => \$quotaSupport },
+
+    { option => "resolution-media-query", desc => "Toggle resolution media query support",
+      define => "ENABLE_RESOLUTION_MEDIA_QUERY", default => (isEfl() || isQt()), value => \$resolutionMediaQuerySupport },
+
+    { option => "request-animation-frame", desc => "Toggle Request Animation Frame support",
+      define => "ENABLE_REQUEST_ANIMATION_FRAME", default => (isAppleMacWebKit() || isGtk() || isEfl() || isBlackBerry()), value => \$requestAnimationFrameSupport },
+
+    { option => "scripted-speech", desc => "Toggle Scripted Speech support",
+      define => "ENABLE_SCRIPTED_SPEECH", default => 0, value => \$scriptedSpeechSupport },
+
+    { option => "shadow-dom", desc => "Toggle Shadow DOM support",
+      define => "ENABLE_SHADOW_DOM", default => (isGtk() || isEfl()), value => \$shadowDOMSupport },
+
+    { option => "shared-workers", desc => "Toggle Shared Workers support",
+      define => "ENABLE_SHARED_WORKERS", default => (isAppleWebKit() || isGtk() || isBlackBerry() || isEfl()), value => \$sharedWorkersSupport },
+
+    { option => "sql-database", desc => "Toggle SQL Database support",
+      define => "ENABLE_SQL_DATABASE", default => 1, value => \$sqlDatabaseSupport },
+
+    { option => "style-scoped", desc => "Toggle Style Scoped support",
+      define => "ENABLE_STYLE_SCOPED", default => isBlackBerry(), value => \$styleScopedSupport },
+
+    { option => "svg", desc => "Toggle SVG support",
+      define => "ENABLE_SVG", default => 1, value => \$svgSupport },
+
+    { option => "svg-dom-objc-bindings", desc => "Toggle SVG DOM ObjC Bindings support",
+      define => "ENABLE_SVG_DOM_OBJC_BINDINGS", default => isAppleMacWebKit(), value => \$svgDOMObjCBindingsSupport },
+
+    { option => "svg-fonts", desc => "Toggle SVG Fonts support",
+      define => "ENABLE_SVG_FONTS", default => 1, value => \$svgFontsSupport },
+
+    { option => "system-malloc", desc => "Toggle system allocator instead of TCmalloc",
+      define => "USE_SYSTEM_MALLOC", default => isWinCE(), value => \$systemMallocSupport },
+
+    { option => "text-autosizing", desc => "Toggle Text Autosizing support",
+      define => "ENABLE_TEXT_AUTOSIZING", default => 0, value => \$textAutosizingSupport },
+
+    { option => "tiled-backing-store", desc => "Toggle Tiled Backing Store support",
+      define => "WTF_USE_TILED_BACKING_STORE", default => (isQt() || isEfl()), value => \$tiledBackingStoreSupport },
+
+    { option => "touch-events", desc => "Toggle Touch Events support",
+      define => "ENABLE_TOUCH_EVENTS", default => (isQt() || isBlackBerry() || isEfl()), value => \$touchEventsSupport },
+
+    { option => "touch-icon-loading", desc => "Toggle Touch Icon Loading Support",
+      define => "ENABLE_TOUCH_ICON_LOADING", default => 0, value => \$touchIconLoadingSupport },
+
+    { option => "vibration", desc => "Toggle Vibration support",
+      define => "ENABLE_VIBRATION", default => (isEfl() || isBlackBerry()), value => \$vibrationSupport },
+
+    { option => "video", desc => "Toggle Video support",
+      define => "ENABLE_VIDEO", default => (isAppleWebKit() || isGtk() || isBlackBerry() || isEfl()), value => \$videoSupport },
+
+    { option => "video-track", desc => "Toggle Video Track support",
+      define => "ENABLE_VIDEO_TRACK", default => (isAppleWebKit() || isGtk() || isEfl() || isBlackBerry()), value => \$videoTrackSupport },
+
+    { option => "webgl", desc => "Toggle WebGL support",
+      define => "ENABLE_WEBGL", default => (isAppleMacWebKit() || isGtk()), value => \$webglSupport },
+
+    { option => "web-audio", desc => "Toggle Web Audio support",
+      define => "ENABLE_WEB_AUDIO", default => 0, value => \$webAudioSupport },
+
+    { option => "web-intents", desc => "Toggle Web Intents support",
+      define => "ENABLE_WEB_INTENTS", default => isEfl(), value => \$webIntentsSupport },
+
+    { option => "web-intents-tag", desc => "Toggle Web Intents Tag support",
+      define => "ENABLE_WEB_INTENTS_TAG", default => isEfl(), value => \$webIntentsTagSupport },
+
+    { option => "web-sockets", desc => "Toggle Web Sockets support",
+      define => "ENABLE_WEB_SOCKETS", default => 1, value => \$webSocketsSupport },
+
+    { option => "web-timing", desc => "Toggle Web Timing support",
+      define => "ENABLE_WEB_TIMING", default => (isBlackBerry() || isGtk() || isEfl()), value => \$webTimingSupport },
+
+    { option => "workers", desc => "Toggle Workers support",
+      define => "ENABLE_WORKERS", default => (isAppleWebKit() || isGtk() || isBlackBerry() || isEfl()), value => \$workersSupport },
+
+    { option => "xhr-response-blob", desc => "Toggle XHR Response BLOB support",
+      define => "ENABLE_XHR_RESPONSE_BLOB", default => isBlackBerry(), value => \$xhrResponseBlobSupport },
+
+    { option => "xhr-timeout", desc => "Toggle XHR Timeout support",
+      define => "ENABLE_XHR_TIMEOUT", default => (isEfl() || isGtk() || isAppleMacWebKit()), value => \$xhrTimeoutSupport },
+
+    { option => "xslt", desc => "Toggle XSLT support",
+      define => "ENABLE_XSLT", default => 1, value => \$xsltSupport },
+);
+
+sub getFeatureOptionList()
+{
+    return @features;
+}
+
+1;
diff --git a/Tools/Scripts/webkitperl/LoadAsModule.pm b/Tools/Scripts/webkitperl/LoadAsModule.pm
new file mode 100644
index 0000000..5c78c0e
--- /dev/null
+++ b/Tools/Scripts/webkitperl/LoadAsModule.pm
@@ -0,0 +1,80 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) 2011 Apple Inc. All rights reserved.
+# Copyright (C) 2011 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:
+# 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.
+#
+# Imports Perl scripts into a package for easy unit testing.
+
+package LoadAsModule;
+
+use strict;
+use warnings;
+
+use File::Spec;
+use FindBin;
+use lib File::Spec->catdir($FindBin::Bin, "..", "..");
+use webkitdirs;
+
+use base 'Exporter';
+use vars qw(@EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
+
+@EXPORT = ();
+@EXPORT_OK = ();
+%EXPORT_TAGS = ();
+$VERSION = '1.0';
+
+sub readFile($);
+
+sub import
+{
+    my ($self, $package, $script) = @_;
+    my $scriptPath = File::Spec->catfile(sourceDir(), "Tools", "Scripts", $script);
+    eval "
+        package $package;
+
+        use strict;
+        use warnings;
+
+        use base 'Exporter';
+        use vars qw(\@EXPORT \@EXPORT_OK \%EXPORT_TAGS \$VERSION);
+
+        \@EXPORT = ();
+        \@EXPORT_OK = ();
+        \%EXPORT_TAGS = ();
+        \$VERSION = '1.0';
+
+        sub {" . readFile($scriptPath) . "}
+    ";
+}
+
+sub readFile($)
+{
+    my $path = shift;
+    local $/ = undef; # Read in the whole file at once.
+    open FILE, "<", $path or die "Cannot open $path: $!";
+    my $contents = <FILE>;
+    close FILE;
+    return $contents;
+};
+
+1;
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/fixChangeLogPatch.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/fixChangeLogPatch.pl
new file mode 100644
index 0000000..261592d
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/fixChangeLogPatch.pl
@@ -0,0 +1,525 @@
+#!/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
+# 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.
+
+# Unit tests of VCSUtils::fixChangeLogPatch().
+
+use strict;
+use warnings;
+
+use Test::More;
+use VCSUtils;
+
+# The source ChangeLog for these tests is the following:
+# 
+# 2009-12-22  Alice  <alice@email.address>
+# 
+#         Reviewed by Ray.
+# 
+#         Changed some code on 2009-12-22.
+# 
+#         * File:
+#         * File2:
+# 
+# 2009-12-21  Alice  <alice@email.address>
+# 
+#         Reviewed by Ray.
+# 
+#         Changed some code on 2009-12-21.
+# 
+#         * File:
+#         * File2:
+
+my @testCaseHashRefs = (
+{ # New test
+    diffName => "fixChangeLogPatch: [no change] In-place change.",
+    inputText => <<'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
+    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 @@
+-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
+    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 @@
+ 
+         * 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
+    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 @@
+ 
+         * 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
+    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 @@
++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
+    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 @@
+ 
+         * File:
+         * File2:
++        * File3:
+ 
+ 2009-12-21  Alice  <alice@email.address>
+ 
+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: New entry inserted in middle.",
+    inputText => <<'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
+    expectedReturn => {
+    patch => <<'END',
+--- ChangeLog
++++ ChangeLog
+@@ -1,3 +1,11 @@
++2009-12-21  Alice  <alice@email.address>
++
++        Reviewed by Ray.
++
++        Changed some more code on 2009-12-21.
++
++        * File:
++
+ 2009-12-21  Alice  <alice@email.address>
+ 
+         Reviewed by Ray.
+END
+    }
+},
+{ # New test
+    diffName => "fixChangeLogPatch: 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 @@
+ 
+ 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
+    expectedReturn => {
+    patch => <<'END',
+--- ChangeLog
++++ ChangeLog
+@@ -1,3 +1,11 @@
++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.
+END
+    }
+},
+{ # New test
+    diffName => "fixChangeLogPatch: Leading context includes first line.",
+    inputText => <<'END',
+--- ChangeLog
++++ ChangeLog
+@@ -1,5 +1,13 @@
+ 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
+    expectedReturn => {
+    patch => <<'END',
+--- ChangeLog
++++ ChangeLog
+@@ -1,3 +1,11 @@
++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.
+END
+    }
+},
+{ # New test
+    diffName => "fixChangeLogPatch: Leading context does not include first line.",
+    inputText => <<'END',
+@@ -2,6 +2,14 @@
+ 
+         Reviewed by Ray.
+ 
++        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.
+ 
+         * File:
+END
+    expectedReturn => {
+    patch => <<'END',
+@@ -1,3 +1,11 @@
++2009-12-22  Alice  <alice@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: 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.
+    inputText => <<'END',
+@@ -1,5 +1,11 @@
+ 2009-12-22  Alice  <alice@email.address>
++ <pretend-whitespace>
++        Reviewed by Ray.
+ 
++        Changed some more code on 2009-12-22.
++
++2009-12-22  Alice  <alice@email.address>
++
+         Reviewed by Ray.
+ 
+         Changed some code on 2009-12-22.
+END
+    expectedReturn => {
+    patch => <<'END',
+@@ -1,3 +1,9 @@
++2009-12-22  Alice  <alice@email.address>
++ <pretend-whitespace>
++        Reviewed by Ray.
++
++        Changed some more code on 2009-12-22.
++
+ 2009-12-22  Alice  <alice@email.address>
+ 
+         Reviewed by Ray.
+END
+    }
+},
+{ # New test
+    diffName => "fixChangeLogPatch: Additional edits after new entry.",
+    inputText => <<'END',
+@@ -2,10 +2,17 @@
+ 
+         Reviewed by Ray.
+ 
++        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.
+ 
+         * File:
+-        * File2:
+ 
+ 2009-12-21  Alice  <alice@email.address>
+ 
+END
+    expectedReturn => {
+    patch => <<'END',
+@@ -1,11 +1,18 @@
++2009-12-22  Alice  <alice@email.address>
++
++        Reviewed by Ray.
++
++        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.
+ 
+         * File:
+-        * File2:
+ 
+ 2009-12-21  Alice  <alice@email.address>
+ 
+END
+    }
+},
+);
+
+my $testCasesCount = @testCaseHashRefs;
+plan(tests => $testCasesCount); # Total number of assertions.
+
+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/Tools/Scripts/webkitperl/VCSUtils_unittest/fixChangeLogPatchThenSetChangeLogDateAndReviewer.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/fixChangeLogPatchThenSetChangeLogDateAndReviewer.pl
new file mode 100644
index 0000000..bbf7df3
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/fixChangeLogPatchThenSetChangeLogDateAndReviewer.pl
@@ -0,0 +1,92 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+# Copyright (C) 2010 Research In Motion Limited. 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.
+
+# Unit tests for setChangeLogDateAndReviewer(fixChangeLogPatch()).
+
+use strict;
+use warnings;
+
+use Test::More;
+use VCSUtils;
+
+my @testCaseHashRefs = (
+{
+    testName => "New entry inserted earlier in the file, but after an entry with the same author and date, patch applied a day later.",
+    reviewer => "Sue",
+    epochTime => 1273414321,
+    patch => <<'END',
+--- ChangeLog
++++ ChangeLog
+@@ -70,6 +70,14 @@
+ 
+ 2010-05-08  Alice  <alice@email.address>
+ 
++        Reviewed by NOBODY (OOPS!).
++
++        Changed some more code on 2010-05-08.
++
++        * File:
++
++2010-05-08  Alice  <alice@email.address>
++
+         Reviewed by Ray.
+ 
+         Changed some code on 2010-05-08.
+END
+    expectedReturn => <<'END',
+--- ChangeLog
++++ ChangeLog
+@@ -1,3 +1,11 @@
++2010-05-09  Alice  <alice@email.address>
++
++        Reviewed by Sue.
++
++        Changed some more code on 2010-05-08.
++
++        * File:
++
+ 2010-05-08  Alice  <alice@email.address>
+ 
+         Reviewed by Ray.
+END
+},
+);
+
+my $testCasesCount = @testCaseHashRefs;
+plan(tests => 1 * $testCasesCount); # Total number of assertions.
+
+foreach my $testCase (@testCaseHashRefs) {
+    my $testNameStart = "setChangeLogDateAndReviewer(fixChangeLogPatch()): $testCase->{testName}: comparing";
+
+    my $patch = $testCase->{patch};
+    my $reviewer = $testCase->{reviewer};
+    my $epochTime = $testCase->{epochTime};
+
+    my $fixedChangeLog = VCSUtils::fixChangeLogPatch($patch);
+    my $got = VCSUtils::setChangeLogDateAndReviewer($fixedChangeLog->{patch}, $reviewer, $epochTime);
+    my $expectedReturn = $testCase->{expectedReturn};
+
+    is($got, $expectedReturn, "$testNameStart return value.");
+}
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/generatePatchCommand.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/generatePatchCommand.pl
new file mode 100644
index 0000000..483a0a8
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/generatePatchCommand.pl
@@ -0,0 +1,87 @@
+#!/usr/bin/perl
+#
+# Copyright (C) 2009, 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+#
+# 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.
+
+# Unit tests of VCSUtils::generatePatchCommand().
+
+use Test::Simple tests => 10;
+use VCSUtils;
+
+# New test
+$title = "generatePatchCommand: Undefined optional arguments.";
+
+my $argsHashRef;
+my ($patchCommand, $isForcing) = VCSUtils::generatePatchCommand($argsHashRef);
+
+ok($patchCommand eq "patch -p0", $title);
+ok($isForcing == 0, $title);
+
+# New test
+$title = "generatePatchCommand: Undefined options.";
+
+my $options;
+$argsHashRef = {options => $options};
+($patchCommand, $isForcing) = VCSUtils::generatePatchCommand($argsHashRef);
+
+ok($patchCommand eq "patch -p0", $title);
+ok($isForcing == 0, $title);
+
+# New test
+$title = "generatePatchCommand: --force and no \"ensure force\".";
+
+$argsHashRef = {options => ["--force"]};
+($patchCommand, $isForcing) = VCSUtils::generatePatchCommand($argsHashRef);
+
+ok($patchCommand eq "patch -p0 --force", $title);
+ok($isForcing == 1, $title);
+
+# New test
+$title = "generatePatchCommand: no --force and \"ensure force\".";
+
+$argsHashRef = {ensureForce => 1};
+($patchCommand, $isForcing) = VCSUtils::generatePatchCommand($argsHashRef);
+
+ok($patchCommand eq "patch -p0 --force", $title);
+ok($isForcing == 1, $title);
+
+# New test
+$title = "generatePatchCommand: \"should reverse\".";
+
+$argsHashRef = {shouldReverse => 1};
+($patchCommand, $isForcing) = VCSUtils::generatePatchCommand($argsHashRef);
+
+ok($patchCommand eq "patch -p0 --reverse", $title);
+
+# New test
+$title = "generatePatchCommand: --fuzz=3, --force.";
+
+$argsHashRef = {options => ["--fuzz=3", "--force"]};
+($patchCommand, $isForcing) = VCSUtils::generatePatchCommand($argsHashRef);
+
+ok($patchCommand eq "patch -p0 --force --fuzz=3", $title);
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/mergeChangeLogs.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/mergeChangeLogs.pl
new file mode 100644
index 0000000..0c8c89c
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/mergeChangeLogs.pl
@@ -0,0 +1,336 @@
+#!/usr/bin/perl
+#
+# 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.
+
+# Unit tests of VCSUtils::mergeChangeLogs().
+
+use strict;
+
+use Test::Simple tests => 16;
+use File::Temp qw(tempfile);
+use VCSUtils;
+
+# Read contents of a file and return it.
+sub readFile($)
+{
+    my ($fileName) = @_;
+
+    local $/;
+    open(FH, "<", $fileName);
+    my $content = <FH>;
+    close(FH);
+
+    return $content;
+}
+
+# Write a temporary file and return the filename.
+sub writeTempFile($$$)
+{
+    my ($name, $extension, $content) = @_;
+
+    my ($FH, $fileName) = tempfile(
+        $name . "-XXXXXXXX",
+        DIR => ($ENV{'TMPDIR'} || $ENV{'TEMP'} || "/tmp"),
+        UNLINK => 0,
+    );
+    print $FH $content;
+    close $FH;
+
+    if ($extension) {
+        my $newFileName = $fileName . $extension;
+        rename($fileName, $newFileName);
+        $fileName = $newFileName;
+    }
+
+    return $fileName;
+}
+
+# --------------------------------------------------------------------------------
+
+{
+    # New test
+    my $title = "mergeChangeLogs: traditional rejected patch success";
+
+    my $fileNewerContent = <<'EOF';
+2010-01-29  Mark Rowe  <mrowe@apple.com>
+
+        Fix the Mac build.
+
+        Disable ENABLE_INDEXED_DATABASE since it is "completely non-functional".
+
+2010-01-29  Simon Hausmann  <simon.hausmann@nokia.com>
+
+        Rubber-stamped by Maciej Stachowiak.
+
+        Fix the ARM build.
+EOF
+    my $fileNewer = writeTempFile("file", "", $fileNewerContent);
+
+    my $fileMineContent = <<'EOF';
+***************
+*** 1,3 ****
+  2010-01-29  Simon Hausmann  <simon.hausmann@nokia.com>
+  
+          Rubber-stamped by Maciej Stachowiak.
+--- 1,9 ----
++ 2010-01-29  Oliver Hunt  <oliver@apple.com>
++ 
++         Reviewed by Darin Adler.
++ 
++         JSC is failing to propagate anonymous slot count on some transitions
++ 
+  2010-01-29  Simon Hausmann  <simon.hausmann@nokia.com>
+  
+          Rubber-stamped by Maciej Stachowiak.
+EOF
+    my $fileMine = writeTempFile("file", ".rej", $fileMineContent);
+    rename($fileMine, $fileNewer . ".rej");
+    $fileMine = $fileNewer . ".rej";
+
+    my $fileOlderContent = $fileNewerContent;
+    my $fileOlder = writeTempFile("file", ".orig", $fileOlderContent);
+    rename($fileOlder, $fileNewer . ".orig");
+    $fileOlder = $fileNewer . ".orig";
+
+    my $exitStatus = mergeChangeLogs($fileMine, $fileOlder, $fileNewer);
+
+    # mergeChangeLogs() should return 1 since the patch succeeded.
+    ok($exitStatus == 1, "$title: should return 1 for success");
+
+    ok(readFile($fileMine) eq $fileMineContent, "$title: \$fileMine should be unchanged");
+    ok(readFile($fileOlder) eq $fileOlderContent, "$title: \$fileOlder should be unchanged");
+
+    my $expectedContent = <<'EOF';
+2010-01-29  Oliver Hunt  <oliver@apple.com>
+
+        Reviewed by Darin Adler.
+
+        JSC is failing to propagate anonymous slot count on some transitions
+
+EOF
+    $expectedContent .= $fileNewerContent;
+    ok(readFile($fileNewer) eq $expectedContent, "$title: \$fileNewer should be updated to include patch");
+
+    unlink($fileMine, $fileOlder, $fileNewer);
+}
+
+# --------------------------------------------------------------------------------
+
+{
+    # New test
+    my $title = "mergeChangeLogs: traditional rejected patch failure";
+
+    my $fileNewerContent = <<'EOF';
+2010-01-29  Mark Rowe  <mrowe@apple.com>
+
+        Fix the Mac build.
+
+        Disable ENABLE_INDEXED_DATABASE since it is "completely non-functional".
+
+2010-01-29  Simon Hausmann  <simon.hausmann@nokia.com>
+
+        Rubber-stamped by Maciej Stachowiak.
+
+        Fix the ARM build.
+EOF
+    my $fileNewer = writeTempFile("file", "", $fileNewerContent);
+
+    my $fileMineContent = <<'EOF';
+***************
+*** 1,9 ****
+- 2010-01-29  Oliver Hunt  <oliver@apple.com>
+- 
+-         Reviewed by Darin Adler.
+- 
+-         JSC is failing to propagate anonymous slot count on some transitions
+- 
+  2010-01-29  Simon Hausmann  <simon.hausmann@nokia.com>
+  
+          Rubber-stamped by Maciej Stachowiak.
+--- 1,3 ----
+  2010-01-29  Simon Hausmann  <simon.hausmann@nokia.com>
+  
+          Rubber-stamped by Maciej Stachowiak.
+EOF
+    my $fileMine = writeTempFile("file", ".rej", $fileMineContent);
+    rename($fileMine, $fileNewer . ".rej");
+    $fileMine = $fileNewer . ".rej";
+
+    my $fileOlderContent = $fileNewerContent;
+    my $fileOlder = writeTempFile("file", ".orig", $fileOlderContent);
+    rename($fileOlder, $fileNewer . ".orig");
+    $fileOlder = $fileNewer . ".orig";
+
+    my $exitStatus = mergeChangeLogs($fileMine, $fileOlder, $fileNewer);
+
+    # mergeChangeLogs() should return 0 since the patch failed.
+    ok($exitStatus == 0, "$title: should return 0 for failure");
+
+    ok(readFile($fileMine) eq $fileMineContent, "$title: \$fileMine should be unchanged");
+    ok(readFile($fileOlder) eq $fileOlderContent, "$title: \$fileOlder should be unchanged");
+    ok(readFile($fileNewer) eq $fileNewerContent, "$title: \$fileNewer should be unchanged");
+
+    unlink($fileMine, $fileOlder, $fileNewer);
+}
+
+# --------------------------------------------------------------------------------
+
+{
+    # New test
+    my $title = "mergeChangeLogs: patch succeeds";
+
+    my $fileMineContent = <<'EOF';
+2010-01-29  Oliver Hunt  <oliver@apple.com>
+
+        Reviewed by Darin Adler.
+
+        JSC is failing to propagate anonymous slot count on some transitions
+
+2010-01-29  Simon Hausmann  <simon.hausmann@nokia.com>
+
+        Rubber-stamped by Maciej Stachowiak.
+
+        Fix the ARM build.
+EOF
+    my $fileMine = writeTempFile("fileMine", "", $fileMineContent);
+
+    my $fileOlderContent = <<'EOF';
+2010-01-29  Simon Hausmann  <simon.hausmann@nokia.com>
+
+        Rubber-stamped by Maciej Stachowiak.
+
+        Fix the ARM build.
+EOF
+    my $fileOlder = writeTempFile("fileOlder", "", $fileOlderContent);
+
+    my $fileNewerContent = <<'EOF';
+2010-01-29  Mark Rowe  <mrowe@apple.com>
+
+        Fix the Mac build.
+
+        Disable ENABLE_INDEXED_DATABASE since it is "completely non-functional".
+
+2010-01-29  Simon Hausmann  <simon.hausmann@nokia.com>
+
+        Rubber-stamped by Maciej Stachowiak.
+
+        Fix the ARM build.
+EOF
+    my $fileNewer = writeTempFile("fileNewer", "", $fileNewerContent);
+
+    my $exitStatus = mergeChangeLogs($fileMine, $fileOlder, $fileNewer);
+
+    # mergeChangeLogs() should return 1 since the patch succeeded.
+    ok($exitStatus == 1, "$title: should return 1 for success");
+
+    ok(readFile($fileMine) eq $fileMineContent, "$title: \$fileMine should be unchanged");
+    ok(readFile($fileOlder) eq $fileOlderContent, "$title: \$fileOlder should be unchanged");
+
+    my $expectedContent = <<'EOF';
+2010-01-29  Oliver Hunt  <oliver@apple.com>
+
+        Reviewed by Darin Adler.
+
+        JSC is failing to propagate anonymous slot count on some transitions
+
+EOF
+    $expectedContent .= $fileNewerContent;
+
+    ok(readFile($fileNewer) eq $expectedContent, "$title: \$fileNewer should be patched");
+
+    unlink($fileMine, $fileOlder, $fileNewer);
+}
+
+# --------------------------------------------------------------------------------
+
+{
+    # New test
+    my $title = "mergeChangeLogs: patch fails";
+
+    my $fileMineContent = <<'EOF';
+2010-01-29  Mark Rowe  <mrowe@apple.com>
+
+        Fix the Mac build.
+
+        Disable ENABLE_INDEXED_DATABASE since it is "completely non-functional".
+
+2010-01-29  Simon Hausmann  <simon.hausmann@nokia.com>
+
+        Rubber-stamped by Maciej Stachowiak.
+
+        Fix the ARM build.
+EOF
+    my $fileMine = writeTempFile("fileMine", "", $fileMineContent);
+
+    my $fileOlderContent = <<'EOF';
+2010-01-29  Mark Rowe  <mrowe@apple.com>
+
+        Fix the Mac build.
+
+        Disable ENABLE_INDEXED_DATABASE since it is "completely non-functional".
+
+2010-01-29  Oliver Hunt  <oliver@apple.com>
+
+        Reviewed by Darin Adler.
+
+        JSC is failing to propagate anonymous slot count on some transitions
+
+2010-01-29  Simon Hausmann  <simon.hausmann@nokia.com>
+
+        Rubber-stamped by Maciej Stachowiak.
+
+        Fix the ARM build.
+EOF
+    my $fileOlder = writeTempFile("fileOlder", "", $fileOlderContent);
+
+    my $fileNewerContent = <<'EOF';
+2010-01-29  Oliver Hunt  <oliver@apple.com>
+
+        Reviewed by Darin Adler.
+
+        JSC is failing to propagate anonymous slot count on some transitions
+
+2010-01-29  Simon Hausmann  <simon.hausmann@nokia.com>
+
+        Rubber-stamped by Maciej Stachowiak.
+
+        Fix the ARM build.
+EOF
+    my $fileNewer = writeTempFile("fileNewer", "", $fileNewerContent);
+
+    my $exitStatus = mergeChangeLogs($fileMine, $fileOlder, $fileNewer);
+
+    # mergeChangeLogs() should return a non-zero exit status since the patch failed.
+    ok($exitStatus == 0, "$title: return non-zero exit status for failure");
+
+    ok(readFile($fileMine) eq $fileMineContent, "$title: \$fileMine should be unchanged");
+    ok(readFile($fileOlder) eq $fileOlderContent, "$title: \$fileOlder should be unchanged");
+
+    # $fileNewer should still exist unchanged because the patch failed
+    ok(readFile($fileNewer) eq $fileNewerContent, "$title: \$fileNewer should be unchanged");
+
+    unlink($fileMine, $fileOlder, $fileNewer);
+}
+
+# --------------------------------------------------------------------------------
+
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/parseChunkRange.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseChunkRange.pl
new file mode 100644
index 0000000..caee50b
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseChunkRange.pl
@@ -0,0 +1,267 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) 2011 Research In Motion Limited. All rights reserved.
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+# Copyright (C) 2012 Daniel Bates (dbates@intudata.com)
+#
+# 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 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 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.
+
+# Unit tests of VCSUtils::parseChunkRange().
+
+use strict;
+use warnings;
+
+use Test::More;
+use VCSUtils;
+
+my @testCaseHashRefs = (
+###
+# Invalid and malformed chunk range
+##
+# FIXME: We should make this set of tests more comprehensive.
+{   # New test
+    testName => "[invalid] Empty string",
+    inputText => "",
+    expectedReturn => []
+},
+{   # New test
+    testName => "[invalid] Bogus chunk range",
+    inputText => "@@ this is not valid @@",
+    expectedReturn => []
+},
+{   # New test
+    testName => "[invalid] Chunk range missing -/+ prefix",
+    inputText => "@@ 0,0 1,4 @@",
+    expectedReturn => []
+},
+{   # New test
+    testName => "[invalid] Chunk range missing commas",
+    inputText => "@@ -0 0 +1 4 @@",
+    expectedReturn => []
+},
+{   # New test
+    testName => "[invalid] Chunk range with swapped old and rew ranges",
+    inputText => "@@ +0,0 -1,4 @@",
+    expectedReturn => []
+},
+{   # New test
+    testName => "[invalid] Chunk range with leading junk",
+    inputText => "leading junk @@ -0,0 +1,4 @@",
+    expectedReturn => []
+},
+###
+#  Simple test cases
+##
+{   # New test
+    testName => "Line count is 0",
+    inputText => "@@ -0,0 +1,4 @@",
+    expectedReturn => [
+{
+    startingLine => 0,
+    lineCount => 0,
+    newStartingLine => 1,
+    newLineCount => 4,
+}
+]
+},
+{   # New test
+    testName => "Line count is 1",
+    inputText => "@@ -1 +1,4 @@",
+    expectedReturn => [
+{
+    startingLine => 1,
+    lineCount => 1,
+    newStartingLine => 1,
+    newLineCount => 4,
+}
+]
+},
+{   # New test
+    testName => "Both original and new line count is 1",
+    inputText => "@@ -1 +1 @@",
+    expectedReturn => [
+{
+    startingLine => 1,
+    lineCount => 1,
+    newStartingLine => 1,
+    newLineCount => 1,
+}
+]
+},
+{   # New test
+    testName => "Line count and new line count > 1",
+    inputText => "@@ -1,2 +1,4 @@",
+    expectedReturn => [
+{
+    startingLine => 1,
+    lineCount => 2,
+    newStartingLine => 1,
+    newLineCount => 4,
+}
+]
+},
+{   # New test
+    testName => "New line count is 0",
+    inputText => "@@ -1,4 +0,0 @@",
+    expectedReturn => [
+{
+    startingLine => 1,
+    lineCount => 4,
+    newStartingLine => 0,
+    newLineCount => 0,
+}
+]
+},
+{   # New test
+    testName => "New line count is 1",
+    inputText => "@@ -1,4 +1 @@",
+    expectedReturn => [
+{
+    startingLine => 1,
+    lineCount => 4,
+    newStartingLine => 1,
+    newLineCount => 1,
+}
+]
+},
+###
+#  Simple SVN 1.7 property diff chunk range tests
+##
+{   # New test
+    testName => "Line count is 0",
+    inputText => "## -0,0 +1,4 ##",
+    chunkSentinel => "##",
+    expectedReturn => [
+{
+    startingLine => 0,
+    lineCount => 0,
+    newStartingLine => 1,
+    newLineCount => 4,
+}
+]
+},
+{   # New test
+    testName => "New line count is 1",
+    inputText => "## -0,0 +1 ##",
+    chunkSentinel => "##",
+    expectedReturn => [
+{
+    startingLine => 0,
+    lineCount => 0,
+    newStartingLine => 1,
+    newLineCount => 1,
+}
+]
+},
+###
+#  Chunk range followed by ending junk
+##
+{   # New test
+    testName => "Line count is 0 and chunk range has ending junk",
+    inputText => "@@ -0,0 +1,4 @@ foo()",
+    expectedReturn => [
+{
+    startingLine => 0,
+    lineCount => 0,
+    newStartingLine => 1,
+    newLineCount => 4,
+}
+]
+},
+{   # New test
+    testName => "Line count is 1 and chunk range has ending junk",
+    inputText => "@@ -1 +1,4 @@ foo()",
+    expectedReturn => [
+{
+    startingLine => 1,
+    lineCount => 1,
+    newStartingLine => 1,
+    newLineCount => 4,
+}
+]
+},
+{   # New test
+    testName => "Both original and new line count is 1 and chunk range has ending junk",
+    inputText => "@@ -1 +1 @@ foo()",
+    expectedReturn => [
+{
+    startingLine => 1,
+    lineCount => 1,
+    newStartingLine => 1,
+    newLineCount => 1,
+}
+]
+},
+{   # New test
+    testName => "Line count and new line count > 1 and chunk range has ending junk",
+    inputText => "@@ -1,2 +1,4 @@ foo()",
+    expectedReturn => [
+{
+    startingLine => 1,
+    lineCount => 2,
+    newStartingLine => 1,
+    newLineCount => 4,
+}
+]
+},
+{   # New test
+    testName => "New line count is 0 and chunk range has ending junk",
+    inputText => "@@ -1,4 +0,0 @@ foo()",
+    expectedReturn => [
+{
+    startingLine => 1,
+    lineCount => 4,
+    newStartingLine => 0,
+    newLineCount => 0,
+}
+]
+},
+{   # New test
+    testName => "New line count is 1 and chunk range has ending junk",
+    inputText => "@@ -1,4 +1 @@ foo()",
+    expectedReturn => [
+{
+    startingLine => 1,
+    lineCount => 4,
+    newStartingLine => 1,
+    newLineCount => 1,
+}
+]
+},
+);
+
+my $testCasesCount = @testCaseHashRefs;
+plan(tests => $testCasesCount);
+
+foreach my $testCase (@testCaseHashRefs) {
+    my $testNameStart = "parseChunkRange(): $testCase->{testName}: comparing";
+
+    my @got = VCSUtils::parseChunkRange($testCase->{inputText}, $testCase->{chunkSentinel});
+    my $expectedReturn = $testCase->{expectedReturn};
+
+    is_deeply(\@got, $expectedReturn, "$testNameStart return value.");
+}
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/parseDiff.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseDiff.pl
new file mode 100644
index 0000000..d416562
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseDiff.pl
@@ -0,0 +1,1277 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+#
+# 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.
+
+# Unit tests of parseDiff().
+
+use strict;
+use warnings;
+
+use Test::More;
+use VCSUtils;
+
+# The array of test cases.
+my @testCaseHashRefs = (
+{
+    # New test
+    diffName => "SVN: simple",
+    inputText => <<'END',
+Index: Makefile
+===================================================================
+--- Makefile	(revision 53052)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKitTools
+ 
+ all:
+END
+    expectedReturn => [
+[{
+    svnConvertedText =>  <<'END', # Same as input text
+Index: Makefile
+===================================================================
+--- Makefile	(revision 53052)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKitTools
+ 
+ all:
+END
+    indexPath => "Makefile",
+    isSvn => 1,
+    numTextChunks => 1,
+    sourceRevision => "53052",
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "SVN: binary file (isBinary true)",
+    inputText => <<'END',
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+Property changes on: test_file.swf
+___________________________________________________________________
+Name: svn:mime-type
+   + application/octet-stream
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+    expectedReturn => [
+[{
+    svnConvertedText =>  <<'END', # Same as input text
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+    indexPath => "test_file.swf",
+    isBinary => 1,
+    isSvn => 1,
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "SVN: binary file (isBinary true) using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+Property changes on: test_file.swf
+___________________________________________________________________
+Name: svn:mime-type
+   + application/octet-stream
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+),
+    expectedReturn => [
+[{
+    svnConvertedText =>  toWindowsLineEndings(<<'END', # Same as input text
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+),
+    indexPath => "test_file.swf",
+    isBinary => 1,
+    isSvn => 1,
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "SVN: leading junk",
+    inputText => <<'END',
+
+LEADING JUNK
+
+Index: Makefile
+===================================================================
+--- Makefile	(revision 53052)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKitTools
+ 
+ all:
+END
+    expectedReturn => [
+[{
+    svnConvertedText =>  <<'END', # Same as input text
+
+LEADING JUNK
+
+Index: Makefile
+===================================================================
+--- Makefile	(revision 53052)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKitTools
+ 
+ all:
+END
+    indexPath => "Makefile",
+    isSvn => 1,
+    numTextChunks => 1,
+    sourceRevision => "53052",
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "SVN: copied file",
+    inputText => <<'END',
+Index: Makefile_new
+===================================================================
+--- Makefile_new	(revision 53131)	(from Makefile:53131)
++++ Makefile_new	(working copy)
+@@ -0,0 +1,1 @@
++MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKitTools
+END
+    expectedReturn => [
+[{
+    copiedFromPath => "Makefile",
+    indexPath => "Makefile_new",
+    sourceRevision => "53131",
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "SVN: two diffs",
+    inputText => <<'END',
+Index: Makefile
+===================================================================
+--- Makefile	(revision 53131)
++++ Makefile	(working copy)
+@@ -1,1 +0,0 @@
+-MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKitTools
+Index: Makefile_new
+===================================================================
+--- Makefile_new	(revision 53131)	(from Makefile:53131)
+END
+    expectedReturn => [
+[{
+    svnConvertedText =>  <<'END',
+Index: Makefile
+===================================================================
+--- Makefile	(revision 53131)
++++ Makefile	(working copy)
+@@ -1,1 +0,0 @@
+-MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKitTools
+END
+    indexPath => "Makefile",
+    isSvn => 1,
+    numTextChunks => 1,
+    sourceRevision => "53131",
+}],
+"Index: Makefile_new\n"],
+    expectedNextLine => "===================================================================\n",
+},
+{
+    # New test
+    diffName => "SVN: SVN diff followed by Git diff", # Should not recognize Git start
+    inputText => <<'END',
+Index: Makefile
+===================================================================
+--- Makefile	(revision 53131)
++++ Makefile	(working copy)
+@@ -1,1 +0,0 @@
+-MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKitTools
+diff --git a/Makefile b/Makefile
+index f5d5e74..3b6aa92 100644
+--- a/Makefile
++++ b/Makefile
+@@ -1,1 1,1 @@ public:
+END
+    expectedReturn => [
+[{
+    svnConvertedText =>  <<'END', # Same as input text
+Index: Makefile
+===================================================================
+--- Makefile	(revision 53131)
++++ Makefile	(working copy)
+@@ -1,1 +0,0 @@
+-MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKitTools
+diff --git a/Makefile b/Makefile
+index f5d5e74..3b6aa92 100644
+--- a/Makefile
++++ b/Makefile
+@@ -1,1 1,1 @@ public:
+END
+    indexPath => "Makefile",
+    isSvn => 1,
+    numTextChunks => 1,
+    sourceRevision => "53131",
+}],
+undef],
+    expectedNextLine => undef,
+},
+####
+# Property Changes: Simple
+##
+{
+    # New test
+    diffName => "SVN: file change diff with property change diff",
+    inputText => <<'END',
+Index: Makefile
+===================================================================
+--- Makefile	(revision 60021)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKit2 WebKitTools 
+
+ all:
+
+Property changes on: Makefile
+___________________________________________________________________
+Name: svn:executable
+   + *
+END
+    expectedReturn => [
+[{
+    svnConvertedText =>  <<'END', # Same as input text
+Index: Makefile
+===================================================================
+--- Makefile	(revision 60021)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKit2 WebKitTools 
+
+ all:
+
+END
+    executableBitDelta => 1,
+    indexPath => "Makefile",
+    isSvn => 1,
+    numTextChunks => 1,
+    sourceRevision => "60021",
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "SVN: file change diff, followed by property change diff on different file",
+    inputText => <<'END',
+Index: Makefile
+===================================================================
+--- Makefile	(revision 60021)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKit2 WebKitTools 
+
+ all:
+
+Property changes on: Makefile.shared
+___________________________________________________________________
+Name: svn:executable
+   + *
+END
+    expectedReturn => [
+[{
+    svnConvertedText =>  <<'END', # Same as input text
+Index: Makefile
+===================================================================
+--- Makefile	(revision 60021)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKit2 WebKitTools 
+
+ all:
+
+END
+    indexPath => "Makefile",
+    isSvn => 1,
+    numTextChunks => 1,
+    sourceRevision => "60021",
+}],
+"Property changes on: Makefile.shared\n"],
+    expectedNextLine => "___________________________________________________________________\n",
+},
+{
+    # New test
+    diffName => "SVN: property diff, followed by file change diff",
+    inputText => <<'END',
+Property changes on: Makefile
+___________________________________________________________________
+Deleted: svn:executable
+   - *
+
+Index: Makefile.shared
+===================================================================
+--- Makefile.shared	(revision 60021)
++++ Makefile.shared	(working copy)
+@@ -1,3 +1,4 @@
++
+SCRIPTS_PATH ?= ../WebKitTools/Scripts
+XCODE_OPTIONS = `perl -I$(SCRIPTS_PATH) -Mwebkitdirs -e 'print XcodeOptionString()'` $(ARGS)
+END
+    expectedReturn => [
+[{
+    executableBitDelta => -1,
+    indexPath => "Makefile",
+    isSvn => 1,
+}],
+"Index: Makefile.shared\n"],
+    expectedNextLine => "===================================================================\n",
+},
+{
+    # New test
+    diffName => "SVN: property diff, followed by file change diff using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Property changes on: Makefile
+___________________________________________________________________
+Deleted: svn:executable
+   - *
+
+Index: Makefile.shared
+===================================================================
+--- Makefile.shared	(revision 60021)
++++ Makefile.shared	(working copy)
+@@ -1,3 +1,4 @@
++
+SCRIPTS_PATH ?= ../WebKitTools/Scripts
+XCODE_OPTIONS = `perl -I$(SCRIPTS_PATH) -Mwebkitdirs -e 'print XcodeOptionString()'` $(ARGS)
+END
+),
+    expectedReturn => [
+[{
+    executableBitDelta => -1,
+    indexPath => "Makefile",
+    isSvn => 1,
+}],
+"Index: Makefile.shared\r\n"],
+    expectedNextLine => "===================================================================\r\n",
+},
+{
+    # New test
+    diffName => "SVN: copied file with property change",
+    inputText => <<'END',
+Index: NMakefile
+===================================================================
+--- NMakefile	(revision 60021)	(from Makefile:60021)
++++ NMakefile	(working copy)
+@@ -0,0 +1,1 @@
++MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKit2 WebKitTools 
+
+Property changes on: NMakefile
+___________________________________________________________________
+Added: svn:executable
+   + *
+END
+    expectedReturn => [
+[{
+    copiedFromPath => "Makefile",
+    executableBitDelta => 1,
+    indexPath => "NMakefile",
+    sourceRevision => "60021",
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "SVN: two consecutive property diffs",
+    inputText => <<'END',
+Property changes on: Makefile
+___________________________________________________________________
+Added: svn:executable
+   + *
+
+
+Property changes on: Makefile.shared
+___________________________________________________________________
+Added: svn:executable
+   + *
+END
+    expectedReturn => [
+[{
+    executableBitDelta => 1,
+    indexPath => "Makefile",
+    isSvn => 1,
+}],
+"Property changes on: Makefile.shared\n"],
+    expectedNextLine => "___________________________________________________________________\n",
+},
+{
+    # New test
+    diffName => "SVN: two consecutive property diffs using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Property changes on: Makefile
+___________________________________________________________________
+Added: svn:executable
+   + *
+
+
+Property changes on: Makefile.shared
+___________________________________________________________________
+Added: svn:executable
+   + *
+END
+),
+    expectedReturn => [
+[{
+    executableBitDelta => 1,
+    indexPath => "Makefile",
+    isSvn => 1,
+}],
+"Property changes on: Makefile.shared\r\n"],
+    expectedNextLine => "___________________________________________________________________\r\n",
+},
+####
+# Property Changes: Binary files
+##
+{
+    # New test
+    diffName => "SVN: binary file with executable bit change",
+    inputText => <<'END',
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+Property changes on: test_file.swf
+___________________________________________________________________
+Name: svn:mime-type
+   + application/octet-stream
+Name: svn:executable
+   + *
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+    expectedReturn => [
+[{
+    svnConvertedText =>  <<'END', # Same as input text
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+    executableBitDelta => 1,
+    indexPath => "test_file.swf",
+    isBinary => 1,
+    isSvn => 1,
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "SVN: binary file with executable bit change usng Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+Property changes on: test_file.swf
+___________________________________________________________________
+Name: svn:mime-type
+   + application/octet-stream
+Name: svn:executable
+   + *
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+),
+    expectedReturn => [
+[{
+    svnConvertedText =>  toWindowsLineEndings(<<'END', # Same as input text
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+),
+    executableBitDelta => 1,
+    indexPath => "test_file.swf",
+    isBinary => 1,
+    isSvn => 1,
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "SVN: binary file followed by property change on different file",
+    inputText => <<'END',
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+Property changes on: test_file.swf
+___________________________________________________________________
+Name: svn:mime-type
+   + application/octet-stream
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+
+Property changes on: Makefile
+___________________________________________________________________
+Added: svn:executable
+   + *
+END
+    expectedReturn => [
+[{
+    svnConvertedText =>  <<'END', # Same as input text
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+
+END
+    indexPath => "test_file.swf",
+    isBinary => 1,
+    isSvn => 1,
+}],
+"Property changes on: Makefile\n"],
+    expectedNextLine => "___________________________________________________________________\n",
+},
+{
+    # New test
+    diffName => "SVN: binary file followed by property change on different file using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+Property changes on: test_file.swf
+___________________________________________________________________
+Name: svn:mime-type
+   + application/octet-stream
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+
+Property changes on: Makefile
+___________________________________________________________________
+Added: svn:executable
+   + *
+END
+),
+    expectedReturn => [
+[{
+    svnConvertedText =>  toWindowsLineEndings(<<'END', # Same as input text
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+
+END
+),
+    indexPath => "test_file.swf",
+    isBinary => 1,
+    isSvn => 1,
+}],
+"Property changes on: Makefile\r\n"],
+    expectedNextLine => "___________________________________________________________________\r\n",
+},
+{
+    # New test
+    diffName => "SVN: binary file followed by file change on different file",
+    inputText => <<'END',
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+Property changes on: test_file.swf
+___________________________________________________________________
+Name: svn:mime-type
+   + application/octet-stream
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+
+Index: Makefile
+===================================================================
+--- Makefile	(revision 60021)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKit2 WebKitTools 
+
+ all:
+END
+    expectedReturn => [
+[{
+    svnConvertedText =>  <<'END', # Same as input text
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+
+END
+    indexPath => "test_file.swf",
+    isBinary => 1,
+    isSvn => 1,
+}],
+"Index: Makefile\n"],
+    expectedNextLine => "===================================================================\n",
+},
+{
+    # New test
+    diffName => "SVN: binary file followed by file change on different file using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+Property changes on: test_file.swf
+___________________________________________________________________
+Name: svn:mime-type
+   + application/octet-stream
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+
+Index: Makefile
+===================================================================
+--- Makefile	(revision 60021)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKit2 WebKitTools 
+
+ all:
+END
+),
+    expectedReturn => [
+[{
+    svnConvertedText =>  toWindowsLineEndings(<<'END', # Same as input text
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+
+END
+),
+    indexPath => "test_file.swf",
+    isBinary => 1,
+    isSvn => 1,
+}],
+"Index: Makefile\r\n"],
+    expectedNextLine => "===================================================================\r\n",
+},
+####
+# Property Changes: File change with property change
+##
+{
+    # New test
+    diffName => "SVN: file change diff with property change, followed by property change diff",
+    inputText => <<'END',
+Index: Makefile
+===================================================================
+--- Makefile	(revision 60021)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKit2 WebKitTools 
+
+ all:
+
+Property changes on: Makefile
+___________________________________________________________________
+Added: svn:executable
+   + *
+
+
+Property changes on: Makefile.shared
+___________________________________________________________________
+Deleted: svn:executable
+   - *
+END
+    expectedReturn => [
+[{
+    svnConvertedText =>  <<'END', # Same as input text
+Index: Makefile
+===================================================================
+--- Makefile	(revision 60021)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKit2 WebKitTools 
+
+ all:
+
+
+
+END
+    executableBitDelta => 1,
+    indexPath => "Makefile",
+    isSvn => 1,
+    numTextChunks => 1,
+    sourceRevision => "60021",
+}],
+"Property changes on: Makefile.shared\n"],
+    expectedNextLine => "___________________________________________________________________\n",
+},
+{
+    # New test
+    diffName => "SVN: file change diff with property change, followed by property change diff using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Index: Makefile
+===================================================================
+--- Makefile	(revision 60021)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKit2 WebKitTools 
+
+ all:
+
+Property changes on: Makefile
+___________________________________________________________________
+Added: svn:executable
+   + *
+
+
+Property changes on: Makefile.shared
+___________________________________________________________________
+Deleted: svn:executable
+   - *
+END
+),
+    expectedReturn => [
+[{
+    svnConvertedText =>  toWindowsLineEndings(<<'END', # Same as input text
+Index: Makefile
+===================================================================
+--- Makefile	(revision 60021)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKit2 WebKitTools 
+
+ all:
+
+
+
+END
+),
+    executableBitDelta => 1,
+    indexPath => "Makefile",
+    isSvn => 1,
+    numTextChunks => 1,
+    sourceRevision => "60021",
+}],
+"Property changes on: Makefile.shared\r\n"],
+    expectedNextLine => "___________________________________________________________________\r\n",
+},
+{
+    # New test
+    diffName => "SVN: file change diff with property change, followed by file change diff",
+    inputText => <<'END',
+Index: Makefile
+===================================================================
+--- Makefile	(revision 60021)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKit2 WebKitTools 
+
+ all:
+
+Property changes on: Makefile
+___________________________________________________________________
+Name: svn:executable
+   - *
+
+Index: Makefile.shared
+===================================================================
+--- Makefile.shared	(revision 60021)
++++ Makefile.shared	(working copy)
+@@ -1,3 +1,4 @@
++
+SCRIPTS_PATH ?= ../WebKitTools/Scripts
+XCODE_OPTIONS = `perl -I$(SCRIPTS_PATH) -Mwebkitdirs -e 'print XcodeOptionString()'` $(ARGS)
+END
+    expectedReturn => [
+[{
+    svnConvertedText =>  <<'END', # Same as input text
+Index: Makefile
+===================================================================
+--- Makefile	(revision 60021)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKit2 WebKitTools 
+
+ all:
+
+
+END
+    executableBitDelta => -1,
+    indexPath => "Makefile",
+    isSvn => 1,
+    numTextChunks => 1,
+    sourceRevision => "60021",
+}],
+"Index: Makefile.shared\n"],
+    expectedNextLine => "===================================================================\n",
+},
+{
+    # New test
+    diffName => "SVN: file change diff with property change, followed by file change diff using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Index: Makefile
+===================================================================
+--- Makefile	(revision 60021)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKit2 WebKitTools 
+
+ all:
+
+Property changes on: Makefile
+___________________________________________________________________
+Name: svn:executable
+   - *
+
+Index: Makefile.shared
+===================================================================
+--- Makefile.shared	(revision 60021)
++++ Makefile.shared	(working copy)
+@@ -1,3 +1,4 @@
++
+SCRIPTS_PATH ?= ../WebKitTools/Scripts
+XCODE_OPTIONS = `perl -I$(SCRIPTS_PATH) -Mwebkitdirs -e 'print XcodeOptionString()'` $(ARGS)
+END
+),
+    expectedReturn => [
+[{
+    svnConvertedText =>  toWindowsLineEndings(<<'END', # Same as input text
+Index: Makefile
+===================================================================
+--- Makefile	(revision 60021)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKit2 WebKitTools 
+
+ all:
+
+
+END
+),
+    executableBitDelta => -1,
+    indexPath => "Makefile",
+    isSvn => 1,
+    numTextChunks => 1,
+    sourceRevision => "60021",
+}],
+"Index: Makefile.shared\r\n"],
+    expectedNextLine => "===================================================================\r\n",
+},
+####
+#    Git test cases
+##
+{
+    # New test
+    diffName => "Git: simple",
+    inputText => <<'END',
+diff --git a/Makefile b/Makefile
+index f5d5e74..3b6aa92 100644
+--- a/Makefile
++++ b/Makefile
+@@ -1,1 +1,1 @@ public:
+END
+    expectedReturn => [
+[{
+    svnConvertedText =>  <<'END',
+Index: Makefile
+index f5d5e74..3b6aa92 100644
+--- Makefile
++++ Makefile
+@@ -1,1 +1,1 @@ public:
+END
+    indexPath => "Makefile",
+    isGit => 1,
+    numTextChunks => 1,
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "Git: Append new line to the end of an existing file",
+    inputText => <<'END',
+diff --git a/foo b/foo
+index 863339f..db418b2 100644
+--- a/foo
++++ b/foo
+@@ -1 +1,2 @@
+ Passed
++
+END
+    expectedReturn => [
+[{
+    svnConvertedText =>  <<'END',
+Index: foo
+index 863339f..db418b2 100644
+--- foo
++++ foo
+@@ -1 +1,2 @@
+ Passed
++
+END
+    indexPath => "foo",
+    isGit => 1,
+    numTextChunks => 1,
+}],
+undef],
+    expectedNextLine => undef,
+},
+{   # New test
+    diffName => "Git: new file",
+    inputText => <<'END',
+diff --git a/foo.h b/foo.h
+new file mode 100644
+index 0000000..3c9f114
+--- /dev/null
++++ b/foo.h
+@@ -0,0 +1,34 @@
++<html>
+diff --git a/bar b/bar
+index d45dd40..3494526 100644
+END
+    expectedReturn => [
+[{
+    svnConvertedText => <<'END',
+Index: foo.h
+new file mode 100644
+index 0000000..3c9f114
+--- foo.h
++++ foo.h
+@@ -0,0 +1,34 @@
++<html>
+END
+    indexPath => "foo.h",
+    isGit => 1,
+    isNew => 1,
+    numTextChunks => 1,
+}],
+"diff --git a/bar b/bar\n"],
+    expectedNextLine => "index d45dd40..3494526 100644\n",
+},
+{   # New test
+    diffName => "Git: file deletion",
+    inputText => <<'END',
+diff --git a/foo b/foo
+deleted file mode 100644
+index 1e50d1d..0000000
+--- a/foo
++++ /dev/null
+@@ -1,1 +0,0 @@
+-line1
+diff --git a/bar b/bar
+index d45dd40..3494526 100644
+END
+    expectedReturn => [
+[{
+    svnConvertedText => <<'END',
+Index: foo
+deleted file mode 100644
+index 1e50d1d..0000000
+--- foo
++++ foo
+@@ -1,1 +0,0 @@
+-line1
+END
+    indexPath => "foo",
+    isDeletion => 1,
+    isGit => 1,
+    numTextChunks => 1,
+}],
+"diff --git a/bar b/bar\n"],
+    expectedNextLine => "index d45dd40..3494526 100644\n",
+},
+{
+    # New test
+    diffName => "Git: Git diff followed by SVN diff", # Should not recognize SVN start
+    inputText => <<'END',
+diff --git a/Makefile b/Makefile
+index f5d5e74..3b6aa92 100644
+--- a/Makefile
++++ b/Makefile
+@@ -1,1 +1,1 @@ public:
+Index: Makefile_new
+===================================================================
+--- Makefile_new	(revision 53131)	(from Makefile:53131)
+END
+    expectedReturn => [
+[{
+    svnConvertedText =>  <<'END',
+Index: Makefile
+index f5d5e74..3b6aa92 100644
+--- Makefile
++++ Makefile
+@@ -1,1 +1,1 @@ public:
+Index: Makefile_new
+===================================================================
+--- Makefile_new	(revision 53131)	(from Makefile:53131)
+END
+    indexPath => "Makefile",
+    isGit => 1,
+    numTextChunks => 1,
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "Git: file that only has an executable bit change",
+    inputText => <<'END',
+diff --git a/foo b/foo
+old mode 100644
+new mode 100755
+END
+    expectedReturn => [
+[{
+    svnConvertedText =>  <<'END',
+Index: foo
+old mode 100644
+new mode 100755
+END
+    executableBitDelta => 1,
+    indexPath => "foo",
+    isGit => 1,
+    numTextChunks => 0,
+}],
+undef],
+    expectedNextLine => undef,
+},
+####
+#    Git test cases: file moves (multiple return values)
+##
+{
+    diffName => "Git: rename (with similarity index 100%)",
+    inputText => <<'END',
+diff --git a/foo b/foo_new
+similarity index 100%
+rename from foo
+rename to foo_new
+diff --git a/bar b/bar
+index d45dd40..3494526 100644
+END
+    expectedReturn => [
+[{
+    indexPath => "foo",
+    isDeletion => 1,
+},
+{
+    copiedFromPath => "foo",
+    indexPath => "foo_new",
+}],
+"diff --git a/bar b/bar\n"],
+    expectedNextLine => "index d45dd40..3494526 100644\n",
+},
+{
+    diffName => "rename (with similarity index < 100%)",
+    inputText => <<'END',
+diff --git a/foo b/foo_new
+similarity index 99%
+rename from foo
+rename to foo_new
+index 1e50d1d..1459d21 100644
+--- a/foo
++++ b/foo_new
+@@ -15,3 +15,4 @@ release r deployment dep deploy:
+ line1
+ line2
+ line3
++line4
+diff --git a/bar b/bar
+index d45dd40..3494526 100644
+END
+    expectedReturn => [
+[{
+    indexPath => "foo",
+    isDeletion => 1,
+},
+{
+    copiedFromPath => "foo",
+    indexPath => "foo_new",
+},
+{
+    indexPath => "foo_new",
+    isGit => 1,
+    numTextChunks => 1,
+    svnConvertedText => <<'END',
+Index: foo_new
+similarity index 99%
+rename from foo
+rename to foo_new
+index 1e50d1d..1459d21 100644
+--- foo_new
++++ foo_new
+@@ -15,3 +15,4 @@ release r deployment dep deploy:
+ line1
+ line2
+ line3
++line4
+END
+}],
+"diff --git a/bar b/bar\n"],
+    expectedNextLine => "index d45dd40..3494526 100644\n",
+},
+{
+    diffName => "rename (with executable bit change)",
+    inputText => <<'END',
+diff --git a/foo b/foo_new
+old mode 100644
+new mode 100755
+similarity index 100%
+rename from foo
+rename to foo_new
+diff --git a/bar b/bar
+index d45dd40..3494526 100644
+END
+    expectedReturn => [
+[{
+    indexPath => "foo",
+    isDeletion => 1,
+},
+{
+    copiedFromPath => "foo",
+    indexPath => "foo_new",
+},
+{
+    executableBitDelta => 1,
+    indexPath => "foo_new",
+    isGit => 1,
+    numTextChunks => 0,
+    svnConvertedText => <<'END',
+Index: foo_new
+old mode 100644
+new mode 100755
+similarity index 100%
+rename from foo
+rename to foo_new
+END
+}],
+"diff --git a/bar b/bar\n"],
+    expectedNextLine => "index d45dd40..3494526 100644\n",
+},
+);
+
+my $testCasesCount = @testCaseHashRefs;
+plan(tests => 2 * $testCasesCount); # Total number of assertions.
+
+foreach my $testCase (@testCaseHashRefs) {
+    my $testNameStart = "parseDiff(): $testCase->{diffName}: comparing";
+
+    my $fileHandle;
+    open($fileHandle, "<", \$testCase->{inputText});
+    my $line = <$fileHandle>;
+
+    my @got = VCSUtils::parseDiff($fileHandle, $line, {"shouldNotUseIndexPathEOL" => 1});
+    my $expectedReturn = $testCase->{expectedReturn};
+
+    is_deeply(\@got, $expectedReturn, "$testNameStart return value.");
+
+    my $gotNextLine = <$fileHandle>;
+    is($gotNextLine, $testCase->{expectedNextLine},  "$testNameStart next read line.");
+}
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/parseDiffHeader.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseDiffHeader.pl
new file mode 100644
index 0000000..8c20f65
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseDiffHeader.pl
@@ -0,0 +1,121 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+#
+# 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 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 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.
+
+# Unit tests of parseDiffHeader().
+
+use strict;
+use warnings;
+
+use Test::More;
+use VCSUtils;
+
+# The unit tests for parseGitDiffHeader() and parseSvnDiffHeader()
+# already thoroughly test parsing each format.
+#
+# For parseDiffHeader(), it should suffice to verify that -- (1) for each
+# format, the method can return non-trivial values back for each key
+# supported by that format (e.g. "sourceRevision" for SVN), (2) the method
+# correctly sets default values when specific key-values are not set
+# (e.g. undef for "sourceRevision" for Git), and (3) key-values unique to
+# this method are set correctly (e.g. "scmFormat").
+my @testCaseHashRefs = (
+####
+#    SVN test cases
+##
+{   # New test
+    diffName => "SVN: non-trivial copiedFromPath and sourceRevision values",
+    inputText => <<'END',
+Index: index_path.py
+===================================================================
+--- index_path.py	(revision 53048)	(from copied_from_path.py:53048)
++++ index_path.py	(working copy)
+@@ -0,0 +1,7 @@
++# Python file...
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: index_path.py
+===================================================================
+--- index_path.py	(revision 53048)	(from copied_from_path.py:53048)
++++ index_path.py	(working copy)
+END
+    copiedFromPath => "copied_from_path.py",
+    indexPath => "index_path.py",
+    isSvn => 1,
+    sourceRevision => 53048,
+},
+"@@ -0,0 +1,7 @@\n"],
+    expectedNextLine => "+# Python file...\n",
+},
+####
+#    Git test cases
+##
+{   # New test case
+    diffName => "Git: Non-zero executable bit",
+    inputText => <<'END',
+diff --git a/foo.exe b/foo.exe
+old mode 100644
+new mode 100755
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: foo.exe
+old mode 100644
+new mode 100755
+END
+    executableBitDelta => 1,
+    indexPath => "foo.exe",
+    isGit => 1,
+},
+undef],
+    expectedNextLine => undef,
+},
+);
+
+my $testCasesCount = @testCaseHashRefs;
+plan(tests => 2 * $testCasesCount); # Total number of assertions.
+
+foreach my $testCase (@testCaseHashRefs) {
+    my $testNameStart = "parseDiffHeader(): $testCase->{diffName}: comparing";
+
+    my $fileHandle;
+    open($fileHandle, "<", \$testCase->{inputText});
+    my $line = <$fileHandle>;
+
+    my @got = VCSUtils::parseDiffHeader($fileHandle, $line);
+    my $expectedReturn = $testCase->{expectedReturn};
+
+    is_deeply(\@got, $expectedReturn, "$testNameStart return value.");
+
+    my $gotNextLine = <$fileHandle>;
+    is($gotNextLine, $testCase->{expectedNextLine},  "$testNameStart next read line.");
+}
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/parseDiffWithMockFiles.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseDiffWithMockFiles.pl
new file mode 100644
index 0000000..ee9fff9
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseDiffWithMockFiles.pl
@@ -0,0 +1,317 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) 2011 Research In Motion Limited. All rights reserved.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+# 
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+# 
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+# Unit tests of parseDiff() with mock files; test override of patch EOL with EOL of target file.
+
+use strict;
+use warnings;
+
+use File::Temp;
+use POSIX qw/getcwd/;
+use Test::More;
+use VCSUtils;
+
+my $gitDiffHeaderForNewFile = <<EOF;
+diff --git a/Makefile b/Makefile
+new file mode 100644
+index 0000000..756e864
+--- /dev/null
++++ b/Makefile
+@@ -0,0 +1,17 @@
+EOF
+
+my $gitDiffHeader = <<EOF;
+diff --git a/Makefile b/Makefile
+index 756e864..04d2ae1 100644
+--- a/Makefile
++++ b/Makefile
+@@ -1,3 +1,4 @@
+EOF
+
+my $svnConvertedGitDiffHeader = <<EOF;
+Index: Makefile
+index 756e864..04d2ae1 100644
+--- Makefile
++++ Makefile
+@@ -1,3 +1,4 @@
+EOF
+
+my $svnConvertedGitDiffHeaderForNewFile = <<EOF;
+Index: Makefile
+new file mode 100644
+index 0000000..756e864
+--- Makefile
++++ Makefile
+@@ -0,0 +1,17 @@
+EOF
+
+my $svnDiffHeaderForNewFile = <<EOF;
+Index: Makefile
+===================================================================
+--- Makefile	(revision 0)
++++ Makefile	(revision 0)
+@@ -0,0 +1,17 @@
+EOF
+
+my $svnDiffHeader = <<EOF;
+Index: Makefile
+===================================================================
+--- Makefile	(revision 53052)
++++ Makefile	(working copy)
+@@ -1,3 +1,4 @@
+EOF
+
+my $diffBody = <<EOF;
++
+ MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKitTools
+ 
+ all:
+EOF
+
+my $MakefileContents = <<EOF;
+MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKitTools 
+
+all:
+EOF
+
+my $mockDir = File::Temp->tempdir("parseDiffXXXX", CLEANUP => 1);
+writeToFile(File::Spec->catfile($mockDir, "MakefileWithUnixEOL"), $MakefileContents);
+writeToFile(File::Spec->catfile($mockDir, "MakefileWithWindowsEOL"), toWindowsLineEndings($MakefileContents));
+
+# The array of test cases.
+my @testCaseHashRefs = (
+###
+# SVN test cases
+##
+{
+    # New test
+    diffName => "SVN: Patch with Unix line endings and IndexPath has Unix line endings",
+    inputText => substituteString($svnDiffHeader, "Makefile", "MakefileWithUnixEOL") . $diffBody,
+    expectedReturn => [
+[{
+    svnConvertedText => substituteString($svnDiffHeader, "Makefile", "MakefileWithUnixEOL") . $diffBody, # Same as input text
+    indexPath => "MakefileWithUnixEOL",
+    isSvn => 1,
+    numTextChunks => 1,
+    sourceRevision => "53052",
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "SVN: Patch with Windows line endings and IndexPath has Unix line endings",
+    inputText => substituteString($svnDiffHeader, "Makefile", "MakefileWithUnixEOL") . toWindowsLineEndings($diffBody),
+    expectedReturn => [
+[{
+    svnConvertedText => substituteString($svnDiffHeader, "Makefile", "MakefileWithUnixEOL") . $diffBody,
+    indexPath => "MakefileWithUnixEOL",
+    isSvn => 1,
+    numTextChunks => 1,
+    sourceRevision => "53052",
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "SVN: Patch with Windows line endings and IndexPath has Windows line endings",
+    inputText => substituteString($svnDiffHeader, "Makefile", "MakefileWithWindowsEOL") . toWindowsLineEndings($diffBody),
+    expectedReturn => [
+[{
+    svnConvertedText => substituteString($svnDiffHeader, "Makefile", "MakefileWithWindowsEOL") . toWindowsLineEndings($diffBody), # Same as input text
+    indexPath => "MakefileWithWindowsEOL",
+    isSvn => 1,
+    numTextChunks => 1,
+    sourceRevision => "53052",
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "SVN: Patch with Unix line endings and IndexPath has Windows line endings",
+    inputText => substituteString($svnDiffHeader, "Makefile", "MakefileWithWindowsEOL") . $diffBody,
+    expectedReturn => [
+[{
+    svnConvertedText => substituteString($svnDiffHeader, "Makefile", "MakefileWithWindowsEOL") . toWindowsLineEndings($diffBody),
+    indexPath => "MakefileWithWindowsEOL",
+    isSvn => 1,
+    numTextChunks => 1,
+    sourceRevision => "53052",
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "SVN: Patch with Unix line endings and nonexistent IndexPath",
+    inputText => substituteString($svnDiffHeaderForNewFile, "Makefile", "NonexistentFile") . $diffBody,
+    expectedReturn => [
+[{
+    svnConvertedText => substituteString($svnDiffHeaderForNewFile, "Makefile", "NonexistentFile") . $diffBody, # Same as input text
+    indexPath => "NonexistentFile",
+    isSvn => 1,
+    isNew => 1,
+    numTextChunks => 1,
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "SVN: Patch with Windows line endings and nonexistent IndexPath",
+    inputText => substituteString($svnDiffHeaderForNewFile, "Makefile", "NonexistentFile") . toWindowsLineEndings($diffBody),
+    expectedReturn => [
+[{
+    svnConvertedText => substituteString($svnDiffHeaderForNewFile, "Makefile", "NonexistentFile") . toWindowsLineEndings($diffBody), # Same as input text
+    indexPath => "NonexistentFile",
+    isSvn => 1,
+    isNew => 1,
+    numTextChunks => 1,
+}],
+undef],
+    expectedNextLine => undef,
+},
+###
+# Git test cases
+##
+{
+    # New test
+    diffName => "Git: Patch with Unix line endings and IndexPath has Unix line endings",
+    inputText => substituteString($gitDiffHeader, "Makefile", "MakefileWithUnixEOL") . $diffBody,
+    expectedReturn => [
+[{
+    svnConvertedText => substituteString($svnConvertedGitDiffHeader, "Makefile", "MakefileWithUnixEOL") . $diffBody, # Same as input text
+    indexPath => "MakefileWithUnixEOL",
+    isGit => 1,
+    numTextChunks => 1,
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "Git: Patch with Windows line endings and IndexPath has Unix line endings",
+    inputText => substituteString($gitDiffHeader, "Makefile", "MakefileWithUnixEOL") . toWindowsLineEndings($diffBody),
+    expectedReturn => [
+[{
+    svnConvertedText => substituteString($svnConvertedGitDiffHeader, "Makefile", "MakefileWithUnixEOL") . $diffBody,
+    indexPath => "MakefileWithUnixEOL",
+    isGit => 1,
+    numTextChunks => 1,
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "Git: Patch with Windows line endings and IndexPath has Windows line endings",
+    inputText => substituteString($gitDiffHeader, "Makefile", "MakefileWithWindowsEOL") . toWindowsLineEndings($diffBody),
+    expectedReturn => [
+[{
+    svnConvertedText => substituteString($svnConvertedGitDiffHeader, "Makefile", "MakefileWithWindowsEOL") . toWindowsLineEndings($diffBody), # Same as input text
+    indexPath => "MakefileWithWindowsEOL",
+    isGit => 1,
+    numTextChunks => 1,
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "Git: Patch with Unix line endings and IndexPath has Windows line endings",
+    inputText => substituteString($gitDiffHeader, "Makefile", "MakefileWithWindowsEOL") . $diffBody,
+    expectedReturn => [
+[{
+    svnConvertedText => substituteString($svnConvertedGitDiffHeader, "Makefile", "MakefileWithWindowsEOL") . toWindowsLineEndings($diffBody),
+    indexPath => "MakefileWithWindowsEOL",
+    isGit => 1,
+    numTextChunks => 1,
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "Git: Patch with Unix line endings and nonexistent IndexPath",
+    inputText => substituteString($gitDiffHeaderForNewFile, "Makefile", "NonexistentFile") . $diffBody,
+    expectedReturn => [
+[{
+    svnConvertedText => substituteString($svnConvertedGitDiffHeaderForNewFile, "Makefile", "NonexistentFile") . $diffBody, # Same as input text
+    indexPath => "NonexistentFile",
+    isGit => 1,
+    isNew => 1,
+    numTextChunks => 1,
+}],
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "Git: Patch with Windows line endings and nonexistent IndexPath",
+    inputText => substituteString($gitDiffHeaderForNewFile, "Makefile", "NonexistentFile") . toWindowsLineEndings($diffBody),
+    expectedReturn => [
+[{
+    svnConvertedText => substituteString($svnConvertedGitDiffHeaderForNewFile, "Makefile", "NonexistentFile") . toWindowsLineEndings($diffBody), # Same as input text
+    indexPath => "NonexistentFile",
+    isGit => 1,
+    isNew => 1,
+    numTextChunks => 1,
+}],
+undef],
+    expectedNextLine => undef,
+},
+);
+
+my $testCasesCount = @testCaseHashRefs;
+plan(tests => 2 * $testCasesCount); # Total number of assertions.
+
+my $savedCWD = getcwd();
+chdir($mockDir) or die;
+foreach my $testCase (@testCaseHashRefs) {
+    my $testNameStart = "parseDiff(): $testCase->{diffName}: comparing";
+
+    my $fileHandle;
+    open($fileHandle, "<", \$testCase->{inputText});
+    my $line = <$fileHandle>;
+
+    my @got = VCSUtils::parseDiff($fileHandle, $line);
+    my $expectedReturn = $testCase->{expectedReturn};
+
+    is_deeply(\@got, $expectedReturn, "$testNameStart return value.");
+
+    my $gotNextLine = <$fileHandle>;
+    is($gotNextLine, $testCase->{expectedNextLine},  "$testNameStart next read line.");
+}
+chdir($savedCWD);
+
+sub substituteString
+{
+    my ($string, $searchString, $replacementString) = @_;
+    $string =~ s/$searchString/$replacementString/g;
+    return $string;
+}
+
+sub writeToFile
+{
+    my ($file, $text) = @_;
+    open(FILE, ">$file") or die;
+    print FILE $text;
+    close(FILE);
+}
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/parseFirstEOL.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseFirstEOL.pl
new file mode 100644
index 0000000..367ad1d
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseFirstEOL.pl
@@ -0,0 +1,63 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) 2011 Research In Motion Limited. All rights reserved.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+# 
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+# 
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+# Unit tests of VCSUtils::parseFirstEOL().
+
+use strict;
+use warnings;
+
+use Test::Simple tests => 7;
+use VCSUtils;
+
+my $title;
+
+# New test
+$title = "parseFirstEOL: Empty string.";
+ok(!defined(firstEOLInString("")), $title);
+
+# New test
+$title = "parseFirstEOL: Line without a line ending character";
+ok(!defined(firstEOLInString("This line doesn't have a line ending character.")), $title);
+
+# New test
+$title = "parseFirstEOL: Line with Windows line ending.";
+ok(firstEOLInString("This line ends with a Windows line ending.\r\n") eq "\r\n", $title);
+
+# New test
+$title = "parseFirstEOL: Line with Unix line ending.";
+ok(firstEOLInString("This line ends with a Unix line ending.\n") eq "\n", $title);
+
+# New test
+$title = "parseFirstEOL: Line with Mac line ending.";
+ok(firstEOLInString("This line ends with a Mac line ending.\r") eq "\r", $title);
+
+# New test
+$title = "parseFirstEOL: Line with Mac line ending followed by line without a line ending.";
+ok(firstEOLInString("This line ends with a Mac line ending.\rThis line doesn't have a line ending.") eq "\r", $title);
+
+# New test
+$title = "parseFirstEOL: Line with a mix of line endings.";
+ok(firstEOLInString("This line contains a mix of line endings.\r\n\r\n\r\r\n\n\n\n") eq "\r\n", $title);
+
+sub firstEOLInString
+{
+    my ($string) = @_;
+    my $fileHandle;
+    open($fileHandle, "<", \$string);
+    return parseFirstEOL($fileHandle);
+}
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/parseGitDiffHeader.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseGitDiffHeader.pl
new file mode 100644
index 0000000..bc0d4d4
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseGitDiffHeader.pl
@@ -0,0 +1,494 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+# Unit tests of parseGitDiffHeader().
+
+use strict;
+use warnings;
+
+use Test::More;
+use VCSUtils;
+
+# The array of test cases.
+my @testCaseHashRefs = (
+{   # New test
+    diffName => "Modified file",
+    inputText => <<'END',
+diff --git a/foo.h b/foo.h
+index f5d5e74..3b6aa92 100644
+--- a/foo.h
++++ b/foo.h
+@@ -1 +1 @@
+-file contents
++new file contents
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: foo.h
+index f5d5e74..3b6aa92 100644
+--- foo.h
++++ foo.h
+END
+    indexPath => "foo.h",
+},
+"@@ -1 +1 @@\n"],
+    expectedNextLine => "-file contents\n",
+},
+{   # New test
+    diffName => "new file",
+    inputText => <<'END',
+diff --git a/foo.h b/foo.h
+new file mode 100644
+index 0000000..3c9f114
+--- /dev/null
++++ b/foo.h
+@@ -0,0 +1,34 @@
++<html>
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: foo.h
+new file mode 100644
+index 0000000..3c9f114
+--- foo.h
++++ foo.h
+END
+    indexPath => "foo.h",
+    isNew => 1,
+},
+"@@ -0,0 +1,34 @@\n"],
+    expectedNextLine => "+<html>\n",
+},
+{   # New test
+    diffName => "file deletion",
+    inputText => <<'END',
+diff --git a/foo b/foo
+deleted file mode 100644
+index 1e50d1d..0000000
+--- a/foo
++++ /dev/null
+@@ -1,1 +0,0 @@
+-line1
+diff --git a/configure.ac b/configure.ac
+index d45dd40..3494526 100644
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: foo
+deleted file mode 100644
+index 1e50d1d..0000000
+--- foo
++++ foo
+END
+    indexPath => "foo",
+    isDeletion => 1,
+},
+"@@ -1,1 +0,0 @@\n"],
+    expectedNextLine => "-line1\n",
+},
+{   # New test
+    diffName => "using --no-prefix",
+    inputText => <<'END',
+diff --git foo.h foo.h
+index c925780..9e65c43 100644
+--- foo.h
++++ foo.h
+@@ -1,3 +1,17 @@
++contents
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: foo.h
+index c925780..9e65c43 100644
+--- foo.h
++++ foo.h
+END
+    indexPath => "foo.h",
+},
+"@@ -1,3 +1,17 @@\n"],
+    expectedNextLine => "+contents\n",
+},
+####
+#    Copy operations
+##
+{   # New test
+    diffName => "copy (with similarity index 100%)",
+    inputText => <<'END',
+diff --git a/foo b/foo_new
+similarity index 100%
+copy from foo
+copy to foo_new
+diff --git a/bar b/bar
+index d45dd40..3494526 100644
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: foo_new
+similarity index 100%
+copy from foo
+copy to foo_new
+END
+    copiedFromPath => "foo",
+    indexPath => "foo_new",
+},
+"diff --git a/bar b/bar\n"],
+    expectedNextLine => "index d45dd40..3494526 100644\n",
+},
+{   # New test
+    diffName => "copy (with similarity index < 100%)",
+    inputText => <<'END',
+diff --git a/foo b/foo_new
+similarity index 99%
+copy from foo
+copy to foo_new
+diff --git a/bar b/bar
+index d45dd40..3494526 100644
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: foo_new
+similarity index 99%
+copy from foo
+copy to foo_new
+END
+    copiedFromPath => "foo",
+    indexPath => "foo_new",
+    isCopyWithChanges => 1,
+},
+"diff --git a/bar b/bar\n"],
+    expectedNextLine => "index d45dd40..3494526 100644\n",
+},
+{   # New test
+    diffName => "rename (with similarity index 100%)",
+    inputText => <<'END',
+diff --git a/foo b/foo_new
+similarity index 100%
+rename from foo
+rename to foo_new
+diff --git a/bar b/bar
+index d45dd40..3494526 100644
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: foo_new
+similarity index 100%
+rename from foo
+rename to foo_new
+END
+    copiedFromPath => "foo",
+    indexPath => "foo_new",
+    shouldDeleteSource => 1,
+},
+"diff --git a/bar b/bar\n"],
+    expectedNextLine => "index d45dd40..3494526 100644\n",
+},
+{   # New test
+    diffName => "rename (with similarity index < 100%)",
+    inputText => <<'END',
+diff --git a/foo b/foo_new
+similarity index 99%
+rename from foo
+rename to foo_new
+index 1e50d1d..1459d21 100644
+--- a/foo
++++ b/foo_new
+@@ -15,3 +15,4 @@ release r deployment dep deploy:
+ line1
+ line2
+ line3
++line4
+diff --git a/bar b/bar
+index d45dd40..3494526 100644
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: foo_new
+similarity index 99%
+rename from foo
+rename to foo_new
+index 1e50d1d..1459d21 100644
+--- foo_new
++++ foo_new
+END
+    copiedFromPath => "foo",
+    indexPath => "foo_new",
+    isCopyWithChanges => 1,
+    shouldDeleteSource => 1,
+},
+"@@ -15,3 +15,4 @@ release r deployment dep deploy:\n"],
+    expectedNextLine => " line1\n",
+},
+{   # New test
+    diffName => "rename (with executable bit change)",
+    inputText => <<'END',
+diff --git a/foo b/foo_new
+old mode 100644
+new mode 100755
+similarity index 100%
+rename from foo
+rename to foo_new
+diff --git a/bar b/bar
+index d45dd40..3494526 100644
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: foo_new
+old mode 100644
+new mode 100755
+similarity index 100%
+rename from foo
+rename to foo_new
+END
+    copiedFromPath => "foo",
+    executableBitDelta => 1,
+    indexPath => "foo_new",
+    isCopyWithChanges => 1,
+    shouldDeleteSource => 1,
+},
+"diff --git a/bar b/bar\n"],
+    expectedNextLine => "index d45dd40..3494526 100644\n",
+},
+####
+#    Binary file test cases
+##
+{
+    # New test case
+    diffName => "New binary file",
+    inputText => <<'END',
+diff --git a/foo.gif b/foo.gif
+new file mode 100644
+index 0000000000000000000000000000000000000000..64a9532e7794fcd791f6f12157406d9060151690
+GIT binary patch
+literal 7
+OcmYex&reDa;sO8*F9L)B
+
+literal 0
+HcmV?d00001
+
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: foo.gif
+new file mode 100644
+index 0000000000000000000000000000000000000000..64a9532e7794fcd791f6f12157406d9060151690
+GIT binary patch
+END
+    indexPath => "foo.gif",
+    isBinary => 1,
+    isNew => 1,
+},
+"literal 7\n"],
+    expectedNextLine => "OcmYex&reDa;sO8*F9L)B\n",
+},
+{
+    # New test case
+    diffName => "Deleted binary file",
+    inputText => <<'END',
+diff --git a/foo.gif b/foo.gif
+deleted file mode 100644
+index 323fae0..0000000
+GIT binary patch
+literal 0
+HcmV?d00001
+
+literal 7
+OcmYex&reDa;sO8*F9L)B
+
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: foo.gif
+deleted file mode 100644
+index 323fae0..0000000
+GIT binary patch
+END
+    indexPath => "foo.gif",
+    isBinary => 1,
+    isDeletion => 1,
+},
+"literal 0\n"],
+    expectedNextLine => "HcmV?d00001\n",
+},
+####
+#    Executable bit test cases
+##
+{
+    # New test case
+    diffName => "Modified executable file",
+    inputText => <<'END',
+diff --git a/foo b/foo
+index d03e242..435ad3a 100755
+--- a/foo
++++ b/foo
+@@ -1 +1 @@
+-file contents
++new file contents
+
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: foo
+index d03e242..435ad3a 100755
+--- foo
++++ foo
+END
+    indexPath => "foo",
+},
+"@@ -1 +1 @@\n"],
+    expectedNextLine => "-file contents\n",
+},
+{
+    # New test case
+    diffName => "Making file executable (last diff)",
+    inputText => <<'END',
+diff --git a/foo.exe b/foo.exe
+old mode 100644
+new mode 100755
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: foo.exe
+old mode 100644
+new mode 100755
+END
+    executableBitDelta => 1,
+    indexPath => "foo.exe",
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test case
+    diffName => "Making file executable (not last diff)",
+    inputText => <<'END',
+diff --git a/foo.exe b/foo.exe
+old mode 100644
+new mode 100755
+diff --git a/another_file.txt b/another_file.txt
+index d03e242..435ad3a 100755
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: foo.exe
+old mode 100644
+new mode 100755
+END
+    executableBitDelta => 1,
+    indexPath => "foo.exe",
+},
+"diff --git a/another_file.txt b/another_file.txt\n"],
+    expectedNextLine => "index d03e242..435ad3a 100755\n",
+},
+{
+    # New test case
+    diffName => "New executable file",
+    inputText => <<'END',
+diff --git a/foo b/foo
+new file mode 100755
+index 0000000..d03e242
+--- /dev/null
++++ b/foo
+@@ -0,0 +1 @@
++file contents
+
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: foo
+new file mode 100755
+index 0000000..d03e242
+--- foo
++++ foo
+END
+    executableBitDelta => 1,
+    indexPath => "foo",
+    isNew => 1,
+},
+"@@ -0,0 +1 @@\n"],
+    expectedNextLine => "+file contents\n",
+},
+{
+    # New test case
+    diffName => "Deleted executable file",
+    inputText => <<'END',
+diff --git a/foo b/foo
+deleted file mode 100755
+index d03e242..0000000
+--- a/foo
++++ /dev/null
+@@ -1 +0,0 @@
+-file contents
+
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: foo
+deleted file mode 100755
+index d03e242..0000000
+--- foo
++++ foo
+END
+    executableBitDelta => -1,
+    indexPath => "foo",
+    isDeletion => 1,
+},
+"@@ -1 +0,0 @@\n"],
+    expectedNextLine => "-file contents\n",
+},
+);
+
+my $testCasesCount = @testCaseHashRefs;
+plan(tests => 2 * $testCasesCount); # Total number of assertions.
+
+foreach my $testCase (@testCaseHashRefs) {
+    my $testNameStart = "parseGitDiffHeader(): $testCase->{diffName}: comparing";
+
+    my $fileHandle;
+    open($fileHandle, "<", \$testCase->{inputText});
+    my $line = <$fileHandle>;
+
+    my @got = VCSUtils::parseGitDiffHeader($fileHandle, $line);
+    my $expectedReturn = $testCase->{expectedReturn};
+
+    is_deeply(\@got, $expectedReturn, "$testNameStart return value.");
+
+    my $gotNextLine = <$fileHandle>;
+    is($gotNextLine, $testCase->{expectedNextLine},  "$testNameStart next read line.");
+}
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/parsePatch.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/parsePatch.pl
new file mode 100644
index 0000000..8aae3d4
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/parsePatch.pl
@@ -0,0 +1,94 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+#
+# 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.
+
+# Unit tests of parseDiffHeader().
+
+use strict;
+use warnings;
+
+use Test::More;
+use VCSUtils;
+
+my @diffHashRefKeys = ( # The hash reference keys to check per diff.
+    "copiedFromPath",
+    "indexPath",
+    "sourceRevision",
+    "svnConvertedText",
+);
+
+# New test
+my $testNameStart = "parsePatch(): [SVN: Rename] ";
+my $patch = <<'END',
+Index: Makefile
+===================================================================
+--- Makefile	(revision 53131)
++++ Makefile	(working copy)
+@@ -1,1 +0,0 @@
+-MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKitTools
+Index: Makefile_new
+===================================================================
+--- Makefile_new	(revision 53131)	(from Makefile:53131)
++++ Makefile_new	(working copy)
+@@ -0,0 +1,1 @@
++MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKitTools
+END
+
+my @expectedDiffHashRefs = (
+{
+    svnConvertedText => <<'END',
+Index: Makefile
+===================================================================
+--- Makefile	(revision 53131)
++++ Makefile	(working copy)
+@@ -1,1 +0,0 @@
+-MODULES = JavaScriptCore JavaScriptGlue WebCore WebKit WebKitTools
+END
+    copiedFromPath => undef,
+    indexPath => "Makefile",
+    sourceRevision => "53131",
+},
+{
+    copiedFromPath => "Makefile",
+    indexPath => "Makefile_new",
+    sourceRevision => "53131",
+},
+);
+
+plan(tests => @expectedDiffHashRefs * @diffHashRefKeys);
+
+my $fileHandle;
+open($fileHandle, "<", \$patch);
+
+my @gotDiffHashRefs = parsePatch($fileHandle);
+
+my $i = 0;
+foreach my $expectedDiffHashRef (@expectedDiffHashRefs) {
+
+    my $gotDiffHashRef = $gotDiffHashRefs[$i++];
+
+    foreach my $diffHashRefKey (@diffHashRefKeys) {
+        my $testName = "${testNameStart}[diff $i] key=\"$diffHashRefKey\"";
+        is($gotDiffHashRef->{$diffHashRefKey}, $expectedDiffHashRef->{$diffHashRefKey}, $testName);
+    }
+}
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/parseSvnDiffFooter.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseSvnDiffFooter.pl
new file mode 100644
index 0000000..7c3d98c
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseSvnDiffFooter.pl
@@ -0,0 +1,443 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) Research in Motion Limited 2010. All Rights Reserved.
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+# Copyright (C) 2012 Daniel Bates (dbates@intudata.com)
+#
+# 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 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 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.
+
+# Unit tests of parseSvnDiffProperties().
+
+use strict;
+use warnings;
+
+use Test::More;
+use VCSUtils;
+
+my @testCaseHashRefs = (
+####
+# Simple test cases
+##
+{
+    # New test
+    diffName => "simple: add svn:executable",
+    inputText => <<'END',
+Property changes on: FileA
+___________________________________________________________________
+Added: svn:executable
+   + *
+END
+    expectedReturn => [
+{
+    propertyPath => "FileA",
+    executableBitDelta => 1,
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "simple: add svn:mergeinfo",
+    inputText => <<'END',
+Property changes on: Makefile
+___________________________________________________________________
+Added: svn:mergeinfo
+   Merged /trunk/Makefile:r33020
+END
+    expectedReturn => [
+{
+    propertyPath => "Makefile",
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "simple: delete svn:mergeinfo",
+    inputText => <<'END',
+Property changes on: Makefile
+___________________________________________________________________
+Deleted: svn:mergeinfo
+   Reverse-merged /trunk/Makefile:r33020
+END
+    expectedReturn => [
+{
+    propertyPath => "Makefile",
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "simple: modified svn:mergeinfo",
+    inputText => <<'END',
+Property changes on: Makefile
+___________________________________________________________________
+Modified: svn:mergeinfo
+   Reverse-merged /trunk/Makefile:r33020
+   Merged /trunk/Makefile:r41697
+END
+    expectedReturn => [
+{
+    propertyPath => "Makefile",
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "simple: delete svn:executable",
+    inputText => <<'END',
+Property changes on: FileA
+___________________________________________________________________
+Deleted: svn:executable
+   - *
+END
+    expectedReturn => [
+{
+    propertyPath => "FileA",
+    executableBitDelta => -1,
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "simple: delete svn:executable using SVN 1.4 syntax",
+    inputText => <<'END',
+Property changes on: FileA
+___________________________________________________________________
+Name: svn:executable
+   - *
+END
+    expectedReturn => [
+{
+    propertyPath => "FileA",
+    executableBitDelta => -1,
+},
+undef],
+    expectedNextLine => undef,
+},
+####
+# Property value followed by empty line and start of next diff
+##
+{
+    # New test
+    diffName => "add svn:executable, followed by empty line and start of next diff",
+    inputText => <<'END',
+Property changes on: FileA
+___________________________________________________________________
+Added: svn:executable
+   + *
+
+Index: Makefile.shared
+END
+    expectedReturn => [
+{
+    propertyPath => "FileA",
+    executableBitDelta => 1,
+},
+"\n"],
+    expectedNextLine => "Index: Makefile.shared\n",
+},
+{
+    # New test
+    diffName => "add svn:executable, followed by empty line and start of next property diff",
+    inputText => <<'END',
+Property changes on: FileA
+___________________________________________________________________
+Added: svn:executable
+   + *
+
+Property changes on: Makefile.shared
+END
+    expectedReturn => [
+{
+    propertyPath => "FileA",
+    executableBitDelta => 1,
+},
+"\n"],
+    expectedNextLine => "Property changes on: Makefile.shared\n",
+},
+####
+# Property value followed by empty line and start of the binary contents
+##
+{
+    # New test
+    diffName => "add svn:executable, followed by empty line and start of binary contents",
+    inputText => <<'END',
+Property changes on: FileA
+___________________________________________________________________
+Added: svn:executable
+   + *
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+    expectedReturn => [
+{
+    propertyPath => "FileA",
+    executableBitDelta => 1,
+},
+"\n"],
+    expectedNextLine => "Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==\n",
+},
+{
+    # New test
+    diffName => "custom property followed by svn:executable, empty line and start of binary contents",
+    inputText => <<'END',
+Property changes on: FileA
+___________________________________________________________________
+Added: documentation
+   + This is an example sentence.
+Added: svn:executable
+   + *
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+    expectedReturn => [
+{
+    propertyPath => "FileA",
+    executableBitDelta => 1,
+},
+"\n"],
+    expectedNextLine => "Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==\n",
+},
+####
+# Successive properties
+##
+{
+    # New test
+    diffName => "svn:executable followed by custom property",
+    inputText => <<'END',
+Property changes on: FileA
+___________________________________________________________________
+Added: svn:executable
+   + *
+Added: documentation
+   + This is an example sentence.
+END
+    expectedReturn => [
+{
+    propertyPath => "FileA",
+    executableBitDelta => 1,
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "svn:executable followed by custom property using SVN 1.7 syntax",
+    inputText => <<'END',
+Property changes on: FileA
+___________________________________________________________________
+Added: svn:executable
+## -0,0 +1 ##
++*
+\ No newline at end of property
+Added: documentation
+## -0,0 +1 ##
++This is an example sentence.
+END
+    expectedReturn => [
+{
+    propertyPath => "FileA",
+    executableBitDelta => 1,
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "svn:executable followed by custom property without newline using SVN 1.7 syntax",
+    inputText => <<'END',
+Property changes on: FileA
+___________________________________________________________________
+Added: svn:executable
+## -0,0 +1 ##
++*
+\ No newline at end of property
+Added: documentation
+## -0,0 +1 ##
++This is an example sentence.
+\ No newline at end of property
+END
+    expectedReturn => [
+{
+    propertyPath => "FileA",
+    executableBitDelta => 1,
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "custom property followed by svn:executable",
+    inputText => <<'END',
+Property changes on: FileA
+___________________________________________________________________
+Added: documentation
+   + This is an example sentence.
+Added: svn:executable
+   + *
+END
+    expectedReturn => [
+{
+    propertyPath => "FileA",
+    executableBitDelta => 1,
+},
+undef],
+    expectedNextLine => undef,
+},
+####
+# Successive properties followed by empty line and start of next diff
+##
+{
+    # New test
+    diffName => "custom property followed by svn:executable, empty line and start of next property diff",
+    inputText => <<'END',
+Property changes on: FileA
+___________________________________________________________________
+Added: documentation
+   + This is an example sentence.
+Added: svn:executable
+   + *
+
+Property changes on: Makefile.shared
+END
+    expectedReturn => [
+{
+    propertyPath => "FileA",
+    executableBitDelta => 1,
+},
+"\n"],
+    expectedNextLine => "Property changes on: Makefile.shared\n",
+},
+{
+    # New test
+    diffName => "custom property followed by svn:executable, empty line and start of next index diff",
+    inputText => <<'END',
+Property changes on: FileA
+___________________________________________________________________
+Added: documentation
+   + This is an example sentence.
+Added: svn:executable
+   + *
+
+Index: Makefile.shared
+END
+    expectedReturn => [
+{
+    propertyPath => "FileA",
+    executableBitDelta => 1,
+},
+"\n"],
+    expectedNextLine => "Index: Makefile.shared\n",
+},
+####
+# Custom properties
+##
+# FIXME: We do not support anything other than the svn:executable property.
+#        We should add support for handling other properties.
+{
+    # New test
+    diffName => "simple: custom property",
+    inputText => <<'END',
+Property changes on: FileA
+___________________________________________________________________
+Name: documentation
+   + This is an example sentence.
+END
+    expectedReturn => [
+{
+    propertyPath => "FileA",
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "custom property followed by custom property",
+    inputText => <<'END',
+Property changes on: FileA
+___________________________________________________________________
+Added: copyright
+   + Copyright (C) Research in Motion Limited 2010. All Rights Reserved.
+Added: documentation
+   + This is an example sentence.
+END
+    expectedReturn => [
+{
+    propertyPath => "FileA",
+},
+undef],
+    expectedNextLine => undef,
+},
+####
+# Malformed property diffs
+##
+# We shouldn't encounter such diffs in practice.
+{
+    # New test
+    diffName => "svn:executable followed by custom property and svn:executable",
+    inputText => <<'END',
+Property changes on: FileA
+___________________________________________________________________
+Added: svn:executable
+   + *
+Added: documentation
+   + This is an example sentence.
+Deleted: svn:executable
+   - *
+END
+    expectedReturn => [
+{
+    propertyPath => "FileA",
+    executableBitDelta => -1,
+},
+undef],
+    expectedNextLine => undef,
+},
+);
+
+my $testCasesCount = @testCaseHashRefs;
+plan(tests => 2 * $testCasesCount); # Total number of assertions.
+
+foreach my $testCase (@testCaseHashRefs) {
+    my $testNameStart = "parseSvnDiffProperties(): $testCase->{diffName}: comparing";
+
+    my $fileHandle;
+    open($fileHandle, "<", \$testCase->{inputText});
+    my $line = <$fileHandle>;
+
+    my @got = VCSUtils::parseSvnDiffProperties($fileHandle, $line);
+    my $expectedReturn = $testCase->{expectedReturn};
+
+    is_deeply(\@got, $expectedReturn, "$testNameStart return value.");
+
+    my $gotNextLine = <$fileHandle>;
+    is($gotNextLine, $testCase->{expectedNextLine},  "$testNameStart next read line.");
+}
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/parseSvnDiffHeader.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseSvnDiffHeader.pl
new file mode 100644
index 0000000..fc357c9
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseSvnDiffHeader.pl
@@ -0,0 +1,288 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+# Copyright (C) 2012 Daniel Bates (dbates@intudata.com)
+#
+# 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 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 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.
+
+# Unit tests of parseSvnDiffHeader().
+
+use strict;
+use warnings;
+
+use Test::More;
+use VCSUtils;
+
+# The array of test cases.
+my @testCaseHashRefs = (
+{
+    # New test
+    diffName => "simple diff",
+    inputText => <<'END',
+Index: WebKitTools/Scripts/VCSUtils.pm
+===================================================================
+--- WebKitTools/Scripts/VCSUtils.pm	(revision 53004)
++++ WebKitTools/Scripts/VCSUtils.pm	(working copy)
+@@ -32,6 +32,7 @@ use strict;
+ use warnings;
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: WebKitTools/Scripts/VCSUtils.pm
+===================================================================
+--- WebKitTools/Scripts/VCSUtils.pm	(revision 53004)
++++ WebKitTools/Scripts/VCSUtils.pm	(working copy)
+END
+    indexPath => "WebKitTools/Scripts/VCSUtils.pm",
+    sourceRevision => "53004",
+},
+"@@ -32,6 +32,7 @@ use strict;\n"],
+    expectedNextLine => " use warnings;\n",
+},
+{
+    # New test
+    diffName => "new file",
+    inputText => <<'END',
+Index: WebKitTools/Scripts/webkitperl/VCSUtils_unittest/parseDiffHeader.pl
+===================================================================
+--- WebKitTools/Scripts/webkitperl/VCSUtils_unittest/parseDiffHeader.pl	(revision 0)
++++ WebKitTools/Scripts/webkitperl/VCSUtils_unittest/parseDiffHeader.pl	(revision 0)
+@@ -0,0 +1,262 @@
++#!/usr/bin/perl -w
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: WebKitTools/Scripts/webkitperl/VCSUtils_unittest/parseDiffHeader.pl
+===================================================================
+--- WebKitTools/Scripts/webkitperl/VCSUtils_unittest/parseDiffHeader.pl	(revision 0)
++++ WebKitTools/Scripts/webkitperl/VCSUtils_unittest/parseDiffHeader.pl	(revision 0)
+END
+    indexPath => "WebKitTools/Scripts/webkitperl/VCSUtils_unittest/parseDiffHeader.pl",
+    isNew => 1,
+},
+"@@ -0,0 +1,262 @@\n"],
+    expectedNextLine => "+#!/usr/bin/perl -w\n",
+},
+{
+    # New test
+    diffName => "new file with spaces in its name",
+    inputText => <<'END',
+Index: WebKit.xcworkspace/xcshareddata/xcschemes/All Source (target WebProcess).xcscheme
+===================================================================
+--- WebKit.xcworkspace/xcshareddata/xcschemes/All Source (target WebProcess).xcscheme	(revision 0)
++++ WebKit.xcworkspace/xcshareddata/xcschemes/All Source (target WebProcess).xcscheme	(revision 0)
+@@ -0,0 +1,8 @@
++<?xml version="1.0" encoding="UTF-8"?>
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: WebKit.xcworkspace/xcshareddata/xcschemes/All Source (target WebProcess).xcscheme
+===================================================================
+--- WebKit.xcworkspace/xcshareddata/xcschemes/All Source (target WebProcess).xcscheme	(revision 0)
++++ WebKit.xcworkspace/xcshareddata/xcschemes/All Source (target WebProcess).xcscheme	(revision 0)
+END
+    indexPath => "WebKit.xcworkspace/xcshareddata/xcschemes/All Source (target WebProcess).xcscheme",
+    isNew => 1,
+},
+"@@ -0,0 +1,8 @@\n"],
+    expectedNextLine => "+<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
+},
+{
+    # New test
+    diffName => "copied file",
+    inputText => <<'END',
+Index: index_path.py
+===================================================================
+--- index_path.py	(revision 53048)	(from copied_from_path.py:53048)
++++ index_path.py	(working copy)
+@@ -0,0 +1,7 @@
++# Python file...
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: index_path.py
+===================================================================
+--- index_path.py	(revision 53048)	(from copied_from_path.py:53048)
++++ index_path.py	(working copy)
+END
+    copiedFromPath => "copied_from_path.py",
+    indexPath => "index_path.py",
+    sourceRevision => 53048,
+},
+"@@ -0,0 +1,7 @@\n"],
+    expectedNextLine => "+# Python file...\n",
+},
+{
+    # New test
+    diffName => "contains \\r\\n lines",
+    inputText => <<END, # No single quotes to allow interpolation of "\r"
+Index: index_path.py\r
+===================================================================\r
+--- index_path.py	(revision 53048)\r
++++ index_path.py	(working copy)\r
+@@ -0,0 +1,7 @@\r
++# Python file...\r
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<END, # No single quotes to allow interpolation of "\r"
+Index: index_path.py\r
+===================================================================\r
+--- index_path.py	(revision 53048)\r
++++ index_path.py	(working copy)\r
+END
+    indexPath => "index_path.py",
+    sourceRevision => 53048,
+},
+"@@ -0,0 +1,7 @@\r\n"],
+    expectedNextLine => "+# Python file...\r\n",
+},
+{
+    # New test
+    diffName => "contains path corrections",
+    inputText => <<'END',
+Index: index_path.py
+===================================================================
+--- bad_path	(revision 53048)	(from copied_from_path.py:53048)
++++ bad_path	(working copy)
+@@ -0,0 +1,7 @@
++# Python file...
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: index_path.py
+===================================================================
+--- index_path.py	(revision 53048)	(from copied_from_path.py:53048)
++++ index_path.py	(working copy)
+END
+    copiedFromPath => "copied_from_path.py",
+    indexPath => "index_path.py",
+    sourceRevision => 53048,
+},
+"@@ -0,0 +1,7 @@\n"],
+    expectedNextLine => "+# Python file...\n",
+},
+####
+#    Binary test cases
+##
+{
+    # New test
+    diffName => "binary file",
+    inputText => <<'END',
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+Property changes on: test_file.swf
+___________________________________________________________________
+Name: svn:mime-type
+   + application/octet-stream
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+END
+    indexPath => "test_file.swf",
+    isBinary => 1,
+},
+"Property changes on: test_file.swf\n"],
+    expectedNextLine => "___________________________________________________________________\n",
+},
+{
+    # New test
+    diffName => "binary file using SVN 1.7 syntax",
+    inputText => <<'END',
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+Index: test_file.swf
+===================================================================
+--- test_file.swf
++++ test_file.swf
+
+Property changes on: test_file.swf
+___________________________________________________________________
+Added: svn:mime-type
+## -0,0 +1 ##
++application/octet-stream
+\ No newline at end of property
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+    expectedReturn => [
+{
+    svnConvertedText => <<'END',
+Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+Index: test_file.swf
+===================================================================
+--- test_file.swf
++++ test_file.swf
+END
+    indexPath => "test_file.swf",
+    isBinary => 1,
+},
+"\n"],
+    expectedNextLine => "Property changes on: test_file.swf\n",
+},
+);
+
+my $testCasesCount = @testCaseHashRefs;
+plan(tests => 2 * $testCasesCount); # Total number of assertions.
+
+foreach my $testCase (@testCaseHashRefs) {
+    my $testNameStart = "parseSvnDiffHeader(): $testCase->{diffName}: comparing";
+
+    my $fileHandle;
+    open($fileHandle, "<", \$testCase->{inputText});
+    my $line = <$fileHandle>;
+
+    my @got = VCSUtils::parseSvnDiffHeader($fileHandle, $line);
+    my $expectedReturn = $testCase->{expectedReturn};
+
+    is_deeply(\@got, $expectedReturn, "$testNameStart return value.");
+
+    my $gotNextLine = <$fileHandle>;
+    is($gotNextLine, $testCase->{expectedNextLine},  "$testNameStart next read line.");
+}
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/parseSvnProperty.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseSvnProperty.pl
new file mode 100644
index 0000000..a613bde
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseSvnProperty.pl
@@ -0,0 +1,805 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) Research in Motion Limited 2010. All Rights Reserved.
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+# Copyright (C) 2012 Daniel Bates (dbates@intudata.com)
+#
+# 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 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 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.
+
+# Unit tests of parseSvnProperty().
+
+use strict;
+use warnings;
+
+use Test::More;
+use VCSUtils;
+
+my @testCaseHashRefs = (
+####
+# Simple test cases
+##
+{
+    # New test
+    diffName => "simple: add svn:executable",
+    inputText => <<'END',
+Added: svn:executable
+   + *
+END
+    expectedReturn => [
+{
+    name => "svn:executable",
+    propertyChangeDelta => 1,
+    value => "*",
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "simple: delete svn:executable",
+    inputText => <<'END',
+Deleted: svn:executable
+   - *
+END
+    expectedReturn => [
+{
+    name => "svn:executable",
+    propertyChangeDelta => -1,
+    value => "*",
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "simple: add svn:mergeinfo",
+    inputText => <<'END',
+Added: svn:mergeinfo
+   Merged /trunk/Makefile:r33020
+END
+    expectedReturn => [
+{
+    name => "svn:mergeinfo",
+    propertyChangeDelta => 1,
+    value => "/trunk/Makefile:r33020",
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "simple: delete svn:mergeinfo",
+    inputText => <<'END',
+Deleted: svn:mergeinfo
+   Reverse-merged /trunk/Makefile:r33020
+END
+    expectedReturn => [
+{
+    name => "svn:mergeinfo",
+    propertyChangeDelta => -1,
+    value => "/trunk/Makefile:r33020",
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "simple: modified svn:mergeinfo",
+    inputText => <<'END',
+Modified: svn:mergeinfo
+   Reverse-merged /trunk/Makefile:r33020
+   Merged /trunk/Makefile:r41697
+END
+    expectedReturn => [
+{
+    name => "svn:mergeinfo",
+    propertyChangeDelta => 1,
+    value => "/trunk/Makefile:r41697",
+},
+undef],
+    expectedNextLine => undef,
+},
+####
+# Using SVN 1.4 syntax
+##
+{
+    # New test
+    diffName => "simple: modified svn:mergeinfo using SVN 1.4 syntax",
+    inputText => <<'END',
+Name: svn:mergeinfo
+   Reverse-merged /trunk/Makefile:r33020
+   Merged /trunk/Makefile:r41697
+END
+    expectedReturn => [
+{
+    name => "svn:mergeinfo",
+    propertyChangeDelta => 1,
+    value => "/trunk/Makefile:r41697",
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "simple: delete svn:executable using SVN 1.4 syntax",
+    inputText => <<'END',
+Name: svn:executable
+   - *
+END
+    expectedReturn => [
+{
+    name => "svn:executable",
+    propertyChangeDelta => -1,
+    value => "*",
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "simple: add svn:executable using SVN 1.4 syntax",
+    inputText => <<'END',
+Name: svn:executable
+   + *
+END
+    expectedReturn => [
+{
+    name => "svn:executable",
+    propertyChangeDelta => 1,
+    value => "*",
+},
+undef],
+    expectedNextLine => undef,
+},
+####
+# Using SVN 1.7 syntax
+##
+{
+    # New test
+    diffName => "simple: add svn:executable using SVN 1.7 syntax",
+    inputText => <<'END',
+Added: svn:executable
+## -0,0 +1 ##
++*
+\ No newline at end of property
+END
+    expectedReturn => [
+{
+    name => "svn:executable",
+    propertyChangeDelta => 1,
+    value => "*",
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "simple: delete svn:executable using SVN 1.7 syntax",
+    inputText => <<'END',
+Deleted: svn:executable
+## -1 +0,0 ##
+-*
+\ No newline at end of property
+END
+    expectedReturn => [
+{
+    name => "svn:executable",
+    propertyChangeDelta => -1,
+    value => "*",
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "add svn:mime-type and add svn:executable using SVN 1.7 syntax",
+    inputText => <<'END',
+Added: svn:mime-type
+## -0,0 +1 ##
++image/png
+\ No newline at end of property
+Added: svn:executable
+## -0,0 +1 ##
++*
+\ No newline at end of property
+END
+    expectedReturn => [
+{
+    name => "svn:mime-type",
+    propertyChangeDelta => 1,
+    value => "image/png",
+},
+"Added: svn:executable\n"],
+    expectedNextLine => "## -0,0 +1 ##\n",
+},
+####
+# Property value followed by empty line and start of next diff
+##
+{
+    # New test
+    diffName => "add svn:executable, followed by empty line and start of next diff",
+    inputText => <<'END',
+Added: svn:executable
+   + *
+
+Index: Makefile.shared
+END
+    expectedReturn => [
+{
+    name => "svn:executable",
+    propertyChangeDelta => 1,
+    value => "*",
+},
+"\n"],
+    expectedNextLine => "Index: Makefile.shared\n",
+},
+{
+    # New test
+    diffName => "add svn:executable, followed by empty line and start of next diff using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Added: svn:executable
+   + *
+
+Index: Makefile.shared
+END
+),
+    expectedReturn => [
+{
+    name => "svn:executable",
+    propertyChangeDelta => 1,
+    value => "*",
+},
+"\r\n"],
+    expectedNextLine => "Index: Makefile.shared\r\n",
+},
+{
+    # New test
+    diffName => "add svn:executable, followed by empty line and start of next property diff",
+    inputText => <<'END',
+Added: svn:executable
+   + *
+
+Property changes on: Makefile.shared
+END
+    expectedReturn => [
+{
+    name => "svn:executable",
+    propertyChangeDelta => 1,
+    value => "*",
+},
+"\n"],
+    expectedNextLine => "Property changes on: Makefile.shared\n",
+},
+{
+    # New test
+    diffName => "add svn:executable, followed by empty line and start of next property diff using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Added: svn:executable
+   + *
+
+Property changes on: Makefile.shared
+END
+),
+    expectedReturn => [
+{
+    name => "svn:executable",
+    propertyChangeDelta => 1,
+    value => "*",
+},
+"\r\n"],
+    expectedNextLine => "Property changes on: Makefile.shared\r\n",
+},
+{
+    # New test
+    diffName => "multi-line '+' change, followed by empty line and start of next diff",
+    inputText => <<'END',
+Name: documentation
+   + A
+long sentence that spans
+multiple lines.
+
+Index: Makefile.shared
+END
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => 1,
+    value => "A\nlong sentence that spans\nmultiple lines.",
+},
+"\n"],
+    expectedNextLine => "Index: Makefile.shared\n",
+},
+{
+    # New test
+    diffName => "multi-line '+' change, followed by empty line and start of next diff using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Name: documentation
+   + A
+long sentence that spans
+multiple lines.
+
+Index: Makefile.shared
+END
+),
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => 1,
+    value => "A\r\nlong sentence that spans\r\nmultiple lines.",
+},
+"\r\n"],
+    expectedNextLine => "Index: Makefile.shared\r\n",
+},
+{
+    # New test
+    diffName => "multi-line '+' change, followed by empty line and start of next property diff",
+    inputText => <<'END',
+Name: documentation
+   + A
+long sentence that spans
+multiple lines.
+
+Property changes on: Makefile.shared
+END
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => 1,
+    value => "A\nlong sentence that spans\nmultiple lines.",
+},
+"\n"],
+    expectedNextLine => "Property changes on: Makefile.shared\n",
+},
+{
+    # New test
+    diffName => "multi-line '+' change, followed by empty line and start of next property diff using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Name: documentation
+   + A
+long sentence that spans
+multiple lines.
+
+Property changes on: Makefile.shared
+END
+),
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => 1,
+    value => "A\r\nlong sentence that spans\r\nmultiple lines.",
+},
+"\r\n"],
+    expectedNextLine => "Property changes on: Makefile.shared\r\n",
+},
+####
+# Property value followed by empty line and start of binary patch
+##
+{
+    # New test
+    diffName => "add svn:executable, followed by empty line and start of binary patch",
+    inputText => <<'END',
+Added: svn:executable
+   + *
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+    expectedReturn => [
+{
+    name => "svn:executable",
+    propertyChangeDelta => 1,
+    value => "*",
+},
+"\n"],
+    expectedNextLine => "Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==\n",
+},
+{
+    # New test
+    diffName => "add svn:executable, followed by empty line and start of binary patch using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Added: svn:executable
+   + *
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+),
+    expectedReturn => [
+{
+    name => "svn:executable",
+    propertyChangeDelta => 1,
+    value => "*",
+},
+"\r\n"],
+    expectedNextLine => "Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==\r\n",
+},
+{
+    # New test
+    diffName => "multi-line '+' change, followed by empty line and start of binary patch",
+    inputText => <<'END',
+Name: documentation
+   + A
+long sentence that spans
+multiple lines.
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => 1,
+    value => "A\nlong sentence that spans\nmultiple lines.",
+},
+"\n"],
+    expectedNextLine => "Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==\n",
+},
+{
+    # New test
+    diffName => "multi-line '+' change, followed by empty line and start of binary patch using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Name: documentation
+   + A
+long sentence that spans
+multiple lines.
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+),
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => 1,
+    value => "A\r\nlong sentence that spans\r\nmultiple lines.",
+},
+"\r\n"],
+    expectedNextLine => "Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==\r\n",
+},
+{
+    # New test
+    diffName => "multi-line '-' change, followed by multi-line '+' change, empty line, and start of binary patch",
+    inputText => <<'END',
+Modified: documentation
+   - A
+long sentence that spans
+multiple lines.
+   + Another
+long sentence that spans
+multiple lines.
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => 1,
+    value => "Another\nlong sentence that spans\nmultiple lines.",
+},
+"\n"],
+    expectedNextLine => "Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==\n",
+},
+{
+    # New test
+    diffName => "multi-line '-' change, followed by multi-line '+' change, empty line, and start of binary patch using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Modified: documentation
+   - A
+long sentence that spans
+multiple lines.
+   + Another
+long sentence that spans
+multiple lines.
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+),
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => 1,
+    value => "Another\r\nlong sentence that spans\r\nmultiple lines.",
+},
+"\r\n"],
+    expectedNextLine => "Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==\r\n",
+},
+####
+# Successive properties
+##
+{
+    # New test
+    diffName => "single-line '+' change followed by custom property with single-line '+' change",
+    inputText => <<'END',
+Added: svn:executable
+   + *
+Added: documentation
+   + A sentence.
+END
+    expectedReturn => [
+{
+    name => "svn:executable",
+    propertyChangeDelta => 1,
+    value => "*",
+},
+"Added: documentation\n"],
+    expectedNextLine => "   + A sentence.\n",
+},
+{
+    # New test
+    diffName => "multi-line '+' change, followed by svn:executable",
+    inputText => <<'END',
+Name: documentation
+   + A
+long sentence that spans
+multiple lines.
+Name: svn:executable
+   + *
+END
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => 1,
+    value => "A\nlong sentence that spans\nmultiple lines.",
+},
+"Name: svn:executable\n"],
+    expectedNextLine => "   + *\n",
+},
+{
+    # New test
+    diffName => "multi-line '-' change, followed by multi-line '+' change and add svn:executable",
+    inputText => <<'END',
+Modified: documentation
+   - A
+long sentence that spans
+multiple lines.
+   + Another
+long sentence that spans
+multiple lines.
+Added: svn:executable
+   + *
+END
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => 1,
+    value => "Another\nlong sentence that spans\nmultiple lines.",
+},
+"Added: svn:executable\n"],
+    expectedNextLine => "   + *\n",
+},
+{
+    # New test
+    diffName => "'Merged' change followed by 'Merged' change",
+    inputText => <<'END',
+Added: svn:mergeinfo
+   Merged /trunk/Makefile:r33020
+   Merged /trunk/Makefile.shared:r58350
+END
+    expectedReturn => [
+{
+    name => "svn:mergeinfo",
+    propertyChangeDelta => 1,
+    value => "/trunk/Makefile.shared:r58350",
+},
+undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "'Reverse-merged' change followed by 'Reverse-merged' change",
+    inputText => <<'END',
+Deleted: svn:mergeinfo
+   Reverse-merged /trunk/Makefile:r33020
+   Reverse-merged /trunk/Makefile.shared:r58350
+END
+    expectedReturn => [
+{
+    name => "svn:mergeinfo",
+    propertyChangeDelta => -1,
+    value => "/trunk/Makefile.shared:r58350",
+},
+undef],
+    expectedNextLine => undef,
+},
+####
+# Property values with trailing new lines.
+##
+# FIXME: We do not support property values with trailing new lines, since it is difficult to
+#        disambiguate them from the empty line that preceeds the contents of a binary patch as
+#        in the test case (above): "multi-line '+' change, followed by empty line and start of binary patch".
+{
+    # New test
+    diffName => "single-line '+' with trailing new line",
+    inputText => <<'END',
+Added: documentation
+   + A sentence.
+
+END
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => 1,
+    value => "A sentence.",
+},
+"\n"],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "single-line '+' with trailing new line using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Added: documentation
+   + A sentence.
+
+END
+),
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => 1,
+    value => "A sentence.",
+},
+"\r\n"],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "single-line '+' with trailing new line, followed by empty line and start of binary patch",
+    inputText => <<'END',
+Added: documentation
+   + A sentence.
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => 1,
+    value => "A sentence.",
+},
+"\n"],
+    expectedNextLine => "\n",
+},
+{
+    # New test
+    diffName => "single-line '+' with trailing new line, followed by empty line and start of binary patch using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Added: documentation
+   + A sentence.
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+),
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => 1,
+    value => "A sentence.",
+},
+"\r\n"],
+    expectedNextLine => "\r\n",
+},
+{
+    # New test
+    diffName => "single-line '-' change with trailing new line, and single-line '+' change",
+    inputText => <<'END',
+Modified: documentation
+   - A long sentence.
+
+   + A sentence.
+END
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => -1, # Since we only interpret the '-' property.
+    value => "A long sentence.",
+},
+"\n"],
+    expectedNextLine => "   + A sentence.\n",
+},
+{
+    # New test
+    diffName => "single-line '-' change with trailing new line, and single-line '+' change using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Modified: documentation
+   - A long sentence.
+
+   + A sentence.
+END
+),
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => -1, # Since we only interpret the '-' property.
+    value => "A long sentence.",
+},
+"\r\n"],
+    expectedNextLine => "   + A sentence.\r\n",
+},
+{
+    # New test
+    diffName => "multi-line '-' change with trailing new line, and multi-line '+' change",
+    inputText => <<'END',
+Modified: documentation
+   - A
+long sentence that spans
+multiple lines.
+
+   + Another
+long sentence that spans
+multiple lines.
+END
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => -1, # Since we only interpret the '-' property.
+    value => "A\nlong sentence that spans\nmultiple lines.",
+},
+"\n"],
+    expectedNextLine => "   + Another\n",
+},
+{
+    # New test
+    diffName => "multi-line '-' change with trailing new line, and multi-line '+' change using Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+Modified: documentation
+   - A
+long sentence that spans
+multiple lines.
+
+   + Another
+long sentence that spans
+multiple lines.
+END
+),
+    expectedReturn => [
+{
+    name => "documentation",
+    propertyChangeDelta => -1, # Since we only interpret the '-' property.
+    value => "A\r\nlong sentence that spans\r\nmultiple lines.",
+},
+"\r\n"],
+    expectedNextLine => "   + Another\r\n",
+},
+);
+
+my $testCasesCount = @testCaseHashRefs;
+plan(tests => 2 * $testCasesCount); # Total number of assertions.
+
+foreach my $testCase (@testCaseHashRefs) {
+    my $testNameStart = "parseSvnProperty(): $testCase->{diffName}: comparing";
+
+    my $fileHandle;
+    open($fileHandle, "<", \$testCase->{inputText});
+    my $line = <$fileHandle>;
+
+    my @got = VCSUtils::parseSvnProperty($fileHandle, $line);
+    my $expectedReturn = $testCase->{expectedReturn};
+
+    is_deeply(\@got, $expectedReturn, "$testNameStart return value.");
+
+    my $gotNextLine = <$fileHandle>;
+    is($gotNextLine, $testCase->{expectedNextLine},  "$testNameStart next read line.");
+}
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/parseSvnPropertyValue.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseSvnPropertyValue.pl
new file mode 100644
index 0000000..33da14a
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/parseSvnPropertyValue.pl
@@ -0,0 +1,257 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) Research in Motion Limited 2010. All Rights Reserved.
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+# Copyright (C) 2012 Daniel Bates (dbates@intudata.com)
+#
+# 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 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 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.
+
+# Unit tests of parseSvnPropertyValue().
+
+use strict;
+use warnings;
+
+use Test::More;
+use VCSUtils;
+
+my @testCaseHashRefs = (
+{
+    # New test
+    diffName => "singe-line '+' change",
+    inputText => <<'END',
+   + *
+END
+    expectedReturn => ["*", undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "single-line '-' change",
+    inputText => <<'END',
+   - *
+END
+    expectedReturn => ["*", undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "'Merged' change",
+    inputText => <<'END',
+   Merged /trunk/Makefile:r33020
+END
+    expectedReturn => ["/trunk/Makefile:r33020", undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "'Reverse-merged' change",
+    inputText => <<'END',
+   Reverse-merged /trunk/Makefile:r33020
+END
+    expectedReturn => ["/trunk/Makefile:r33020", undef],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "single-line '-' change followed by empty line with Unix line endings",
+    inputText => <<'END',
+   - *
+
+END
+    expectedReturn => ["*", "\n"],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "single-line '-' change followed by empty line with Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+   - *
+
+END
+),
+    expectedReturn => ["*", "\r\n"],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "single-line '-' change followed by the next property",
+    inputText => <<'END',
+   - *
+Deleted: svn:executable
+END
+    expectedReturn => ["*", "Deleted: svn:executable\n"],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "multi-line '+' change and start of binary patch",
+    inputText => <<'END',
+   + A
+long sentence that spans
+multiple lines.
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+    expectedReturn => ["A\nlong sentence that spans\nmultiple lines.", "\n"],
+    expectedNextLine => "Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==\n",
+},
+{
+    # New test
+    diffName => "multi-line '+' change and start of binary patch with Windows line endings",
+    inputText => toWindowsLineEndings(<<'END',
+   + A
+long sentence that spans
+multiple lines.
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+END
+),
+    expectedReturn => ["A\r\nlong sentence that spans\r\nmultiple lines.", "\r\n"],
+    expectedNextLine => "Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==\r\n",
+},
+{
+    # New test
+    diffName => "multi-line '-' change followed by '+' single-line change",
+    inputText => <<'END',
+   - A
+long sentence that spans
+multiple lines.
+   + A single-line.
+END
+    expectedReturn => ["A\nlong sentence that spans\nmultiple lines.", "   + A single-line.\n"],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "multi-line '-' change followed by the next property",
+    inputText => <<'END',
+   - A
+long sentence that spans
+multiple lines.
+Added: svn:executable
+END
+    expectedReturn => ["A\nlong sentence that spans\nmultiple lines.", "Added: svn:executable\n"],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "multi-line '-' change followed by '+' multi-line change",
+    inputText => <<'END',
+   - A
+long sentence that spans
+multiple lines.
+   + Another
+long sentence that spans
+multiple lines.
+END
+    expectedReturn => ["A\nlong sentence that spans\nmultiple lines.", "   + Another\n"],
+    expectedNextLine => "long sentence that spans\n",
+},
+{
+    # New test
+    diffName => "'Reverse-merged' change followed by 'Merge' change",
+    inputText => <<'END',
+   Reverse-merged /trunk/Makefile:r33020
+   Merged /trunk/Makefile:r41697
+END
+    expectedReturn => ["/trunk/Makefile:r33020", "   Merged /trunk/Makefile:r41697\n"],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "'Merged' change followed by 'Merge' change",
+    inputText => <<'END',
+   Merged /trunk/Makefile:r33020
+   Merged /trunk/Makefile.shared:r58350
+END
+    expectedReturn => ["/trunk/Makefile:r33020", "   Merged /trunk/Makefile.shared:r58350\n"],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "'Reverse-merged' change followed by 'Reverse-merged' change",
+    inputText => <<'END',
+   Reverse-merged /trunk/Makefile:r33020
+   Reverse-merged /trunk/Makefile.shared:r58350
+END
+    expectedReturn => ["/trunk/Makefile:r33020", "   Reverse-merged /trunk/Makefile.shared:r58350\n"],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "'Reverse-merged' change followed by 'Reverse-merged' change followed by 'Merged' change",
+    inputText => <<'END',
+   Reverse-merged /trunk/Makefile:r33020
+   Reverse-merged /trunk/Makefile.shared:r58350
+   Merged /trunk/ChangeLog:r64190
+END
+    expectedReturn => ["/trunk/Makefile:r33020", "   Reverse-merged /trunk/Makefile.shared:r58350\n"],
+    expectedNextLine => "   Merged /trunk/ChangeLog:r64190\n",
+},
+##
+# Using SVN 1.7 syntax
+##
+{
+    # New test
+    diffName => "singe-line '+' change using SVN 1.7 syntax",
+    inputText => <<'END',
++*
+\ No newline at end of property
+END
+    expectedReturn => ["*", "\\ No newline at end of property\n"],
+    expectedNextLine => undef,
+},
+{
+    # New test
+    diffName => "single-line '-' change using SVN 1.7 syntax",
+    inputText => <<'END',
+-*
+\ No newline at end of property
+END
+    expectedReturn => ["*", "\\ No newline at end of property\n"],
+    expectedNextLine => undef,
+},
+);
+
+my $testCasesCount = @testCaseHashRefs;
+plan(tests => 2 * $testCasesCount); # Total number of assertions.
+
+foreach my $testCase (@testCaseHashRefs) {
+    my $testNameStart = "parseSvnPropertyValue(): $testCase->{diffName}: comparing";
+
+    my $fileHandle;
+    open($fileHandle, "<", \$testCase->{inputText});
+    my $line = <$fileHandle>;
+
+    my @got = VCSUtils::parseSvnPropertyValue($fileHandle, $line);
+    my $expectedReturn = $testCase->{expectedReturn};
+
+    is_deeply(\@got, $expectedReturn, "$testNameStart return value.");
+
+    my $gotNextLine = <$fileHandle>;
+    is($gotNextLine, $testCase->{expectedNextLine},  "$testNameStart next read line.");
+}
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/prepareParsedPatch.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/prepareParsedPatch.pl
new file mode 100644
index 0000000..a7ae807
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/prepareParsedPatch.pl
@@ -0,0 +1,136 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+# Unit tests of prepareParsedPatch().
+
+use strict;
+use warnings;
+
+use Test::More;
+use VCSUtils;
+
+my $diffHashRef1 = { # not a copy, no source revision
+    copiedFromPath => undef,
+    indexPath => "indexPath1",
+    sourceRevision => undef,
+    svnConvertedText => "diff1",
+};
+my $diffHashRef2 = { # not a copy, has source revision
+    copiedFromPath => undef,
+    indexPath => "indexPath2",
+    sourceRevision => 20,
+    svnConvertedText => "diff2",
+};
+my $diffHashRef3 = { # a copy (copies always have source revision)
+    copiedFromPath => "sourcePath3",
+    indexPath => "indexPath2", # Deliberately choosing same as $diffHashRef2
+    sourceRevision => 3,
+    svnConvertedText => "diff3",
+};
+
+my @testCases = (
+{
+    # New test
+    testName => "zero diffs: empty array",
+    diffHashRefsInput => [],
+    expected => {
+        copyDiffHashRefs => [],
+        nonCopyDiffHashRefs => [],
+        sourceRevisionHash => {},
+    },
+},
+{
+    # New test
+    testName => "one diff: non-copy, no revision",
+    diffHashRefsInput => [$diffHashRef1],
+    expected => {
+        copyDiffHashRefs => [],
+        nonCopyDiffHashRefs => [$diffHashRef1],
+        sourceRevisionHash => {},
+    },
+},
+{
+    # New test
+    testName => "one diff: non-copy, has revision",
+    diffHashRefsInput => [$diffHashRef2],
+    expected => {
+        copyDiffHashRefs => [],
+        nonCopyDiffHashRefs => [$diffHashRef2],
+        sourceRevisionHash => {
+            "indexPath2" => 20,
+        }
+    },
+},
+{
+    # New test
+    testName => "one diff: copy (has revision)",
+    diffHashRefsInput => [$diffHashRef3],
+    expected => {
+        copyDiffHashRefs => [$diffHashRef3],
+        nonCopyDiffHashRefs => [],
+        sourceRevisionHash => {
+            "sourcePath3" => 3,
+        }
+    },
+},
+{
+    # New test
+    testName => "two diffs: two non-copies",
+    diffHashRefsInput => [$diffHashRef1, $diffHashRef2],
+    expected => {
+        copyDiffHashRefs => [],
+        nonCopyDiffHashRefs => [$diffHashRef1, $diffHashRef2],
+        sourceRevisionHash => {
+            "indexPath2" => 20,
+        }
+    },
+},
+{
+    # New test
+    testName => "two diffs: non-copy and copy",
+    diffHashRefsInput => [$diffHashRef2, $diffHashRef3],
+    expected => {
+        copyDiffHashRefs => [$diffHashRef3],
+        nonCopyDiffHashRefs => [$diffHashRef2],
+        sourceRevisionHash => {
+            "sourcePath3" => 3,
+            "indexPath2" => 20,
+        }
+    },
+},
+);
+
+my $testCasesCount = @testCases;
+plan(tests => $testCasesCount);
+
+foreach my $testCase (@testCases) {
+    my $testName = $testCase->{testName};
+    my @diffHashRefs = @{$testCase->{diffHashRefsInput}};
+    my $expected = $testCase->{expected};
+
+    my $got = prepareParsedPatch(0, @diffHashRefs);
+
+    is_deeply($got, $expected, $testName);
+}
+
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/removeEOL.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/removeEOL.pl
new file mode 100644
index 0000000..6880214
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/removeEOL.pl
@@ -0,0 +1,56 @@
+#!/usr/bin/perl
+#
+# 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 are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Research In Motion Limited 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.
+
+# Unit tests of VCSUtils::removeEOL().
+
+use Test::Simple tests => 5;
+use VCSUtils;
+
+my $title;
+
+# New test
+$title = "removeEOL: Undefined argument.";
+ok(removeEOL(undef) eq "", $title);
+
+# New test
+$title = "removeEOL: Line with Windows line ending.";
+ok(removeEOL("This line ends with a Windows line ending.\r\n") eq "This line ends with a Windows line ending.", $title);
+
+# New test
+$title = "removeEOL: Line with Unix line ending.";
+ok(removeEOL("This line ends with a Unix line ending.\n") eq "This line ends with a Unix line ending.", $title);
+
+# New test
+$title = "removeEOL: Line with Mac line ending.";
+ok(removeEOL("This line ends with a Mac line ending.\r") eq "This line ends with a Mac line ending.", $title);
+
+# New test
+$title = "removeEOL: Line with a mix of line endings.";
+ok(removeEOL("This line contains a mix of line endings.\r\n\r\n\r\r\n\n\n\n") eq "This line contains a mix of line endings.", $title);
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/runCommand.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/runCommand.pl
new file mode 100644
index 0000000..4514074
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/runCommand.pl
@@ -0,0 +1,75 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) 2012 Daniel Bates (dbates@intudata.com). 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.
+
+# Unit tests of VCSUtils::runCommand().
+
+use strict;
+use warnings;
+
+use Test::More;
+use VCSUtils;
+
+use constant ENOENT => 2; # See <errno.h>
+
+# The array of test cases.
+my @testCaseHashRefs = (
+{
+    # New test
+    testName => "Simple",
+    inputArgs => ["echo", "hello"],
+    expectedReturn => {
+        exitStatus => 0,
+        stdout => "hello\n"
+    }
+},
+{
+    # New test
+    testName => "Multiple commands",
+    inputArgs => ["echo", "first-command;echo second-command"],
+    expectedReturn => {
+        exitStatus => 0,
+        stdout => "first-command;echo second-command\n"
+    }
+},
+{
+    # New test
+    testName => "Non-existent command",
+    inputArgs => ["/usr/bin/non-existent-command"],
+    expectedReturn => {
+        exitStatus => ENOENT
+    }
+}
+);
+
+my $testCasesCount = @testCaseHashRefs;
+plan(tests => $testCasesCount); # Total number of assertions.
+
+foreach my $testCase (@testCaseHashRefs) {
+    my $testNameStart = "runCommand(): $testCase->{testName}: comparing";
+
+    my $got = VCSUtils::runCommand(@{$testCase->{inputArgs}});
+    my $expectedReturn = $testCase->{expectedReturn};
+
+    is_deeply($got, $expectedReturn, "$testNameStart return value.");
+}
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/runPatchCommand.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/runPatchCommand.pl
new file mode 100644
index 0000000..5acc517
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/runPatchCommand.pl
@@ -0,0 +1,92 @@
+#!/usr/bin/perl
+#
+# Copyright (C) 2009, 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+#
+# 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.
+
+# Unit tests of VCSUtils::runPatchCommand().
+
+use Test::Simple tests => 4;
+use VCSUtils;
+
+# New test
+$title = "runPatchCommand: Unsuccessful patch, forcing.";
+
+# Since $patch has no "Index:" path, passing this to runPatchCommand
+# should not affect any files.
+my $patch = <<'END';
+Garbage patch contents
+END
+
+# We call via callSilently() to avoid output like the following to STDERR:
+# patch: **** Only garbage was found in the patch input.
+$argsHashRef = {ensureForce => 1};
+$exitStatus = callSilently(\&runPatchCommand, $patch, ".", "file_to_patch.txt", $argsHashRef);
+
+ok($exitStatus != 0, $title);
+
+# New test
+$title = "runPatchCommand: New file, --dry-run.";
+
+# This file should not exist after the tests, but we take care with the
+# file name and contents just in case.
+my $fileToPatch = "temp_OK_TO_ERASE__README_FOR_MORE.txt";
+$patch = <<END;
+Index: $fileToPatch
+===================================================================
+--- $fileToPatch	(revision 0)
++++ $fileToPatch	(revision 0)
+@@ -0,0 +1,5 @@
++This is a test file for WebKitTools/Scripts/VCSUtils_unittest.pl.
++This file should not have gotten created on your system.
++If it did, some unit tests don't seem to be working quite right:
++It would be great if you could file a bug report. Thanks!
++---------------------------------------------------------------------
+END
+
+# --dry-run prevents creating any files.
+# --silent suppresses the success message to STDOUT.
+$argsHashRef = {options => ["--dry-run", "--silent"]};
+$exitStatus = runPatchCommand($patch, ".", $fileToPatch, $argsHashRef);
+
+ok($exitStatus == 0, $title);
+
+# New test
+$title = "runPatchCommand: New file: \"$fileToPatch\".";
+
+$argsHashRef = {options => ["--silent"]};
+$exitStatus = runPatchCommand($patch, ".", $fileToPatch, $argsHashRef);
+
+ok($exitStatus == 0, $title);
+
+# New test
+$title = "runPatchCommand: Reverse new file (clean up previous).";
+
+$argsHashRef = {shouldReverse => 1,
+                options => ["--silent", "--remove-empty-files"]}; # To clean up.
+$exitStatus = runPatchCommand($patch, ".", $fileToPatch, $argsHashRef);
+ok($exitStatus == 0, $title);
diff --git a/Tools/Scripts/webkitperl/VCSUtils_unittest/setChangeLogDateAndReviewer.pl b/Tools/Scripts/webkitperl/VCSUtils_unittest/setChangeLogDateAndReviewer.pl
new file mode 100644
index 0000000..076d88c
--- /dev/null
+++ b/Tools/Scripts/webkitperl/VCSUtils_unittest/setChangeLogDateAndReviewer.pl
@@ -0,0 +1,128 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+# Unit tests of setChangeLogDateAndReviewer().
+
+use strict;
+use warnings;
+
+use Test::More;
+use VCSUtils;
+
+my @testCaseHashRefs = (
+{
+    testName => "reviewer defined and \"NOBODY (OOPS!)\" in leading junk",
+    reviewer => "John Doe",
+    epochTime => 1273414321,
+    patch => <<'END',
+Subject: [PATCH]
+
+Reviewed by NOBODY (OOPS!).
+
+diff --git a/WebCore/ChangeLog b/WebCore/ChangeLog
+--- a/WebCore/ChangeLog
++++ b/WebCore/ChangeLog
+@@ -1,3 +1,15 @@
++2010-05-08  Chris Jerdonek  <cjerdonek@webkit.org>
++
++        Reviewed by NOBODY (OOPS!).
++
+ 2010-05-08  Chris Jerdonek  <cjerdonek@webkit.org>
+ 
+         Reviewed by Jane Doe.
+END
+    expectedReturn => <<'END',
+Subject: [PATCH]
+
+Reviewed by NOBODY (OOPS!).
+
+diff --git a/WebCore/ChangeLog b/WebCore/ChangeLog
+--- a/WebCore/ChangeLog
++++ b/WebCore/ChangeLog
+@@ -1,3 +1,15 @@
++2010-05-09  Chris Jerdonek  <cjerdonek@webkit.org>
++
++        Reviewed by John Doe.
++
+ 2010-05-08  Chris Jerdonek  <cjerdonek@webkit.org>
+ 
+         Reviewed by Jane Doe.
+END
+},
+{
+    testName => "reviewer not defined and \"NOBODY (OOPS!)\" in leading junk",
+    reviewer => undef,
+    epochTime => 1273414321,
+    patch => <<'END',
+Subject: [PATCH]
+
+Reviewed by NOBODY (OOPS!).
+
+diff --git a/WebCore/ChangeLog b/WebCore/ChangeLog
+--- a/WebCore/ChangeLog
++++ b/WebCore/ChangeLog
+@@ -1,3 +1,15 @@
++2010-05-08  Chris Jerdonek  <cjerdonek@webkit.org>
++
++        Reviewed by NOBODY (OOPS!).
++
+ 2010-05-08  Chris Jerdonek  <cjerdonek@webkit.org>
+ 
+         Reviewed by Jane Doe.
+END
+    expectedReturn => <<'END',
+Subject: [PATCH]
+
+Reviewed by NOBODY (OOPS!).
+
+diff --git a/WebCore/ChangeLog b/WebCore/ChangeLog
+--- a/WebCore/ChangeLog
++++ b/WebCore/ChangeLog
+@@ -1,3 +1,15 @@
++2010-05-09  Chris Jerdonek  <cjerdonek@webkit.org>
++
++        Reviewed by NOBODY (OOPS!).
++
+ 2010-05-08  Chris Jerdonek  <cjerdonek@webkit.org>
+ 
+         Reviewed by Jane Doe.
+END
+},
+);
+
+my $testCasesCount = @testCaseHashRefs;
+plan(tests => 1 * $testCasesCount); # Total number of assertions.
+
+foreach my $testCase (@testCaseHashRefs) {
+    my $testNameStart = "setChangeLogDateAndReviewer(): $testCase->{testName}: comparing";
+
+    my $patch = $testCase->{patch};
+    my $reviewer = $testCase->{reviewer};
+    my $epochTime = $testCase->{epochTime};
+
+    my $got = VCSUtils::setChangeLogDateAndReviewer($patch, $reviewer, $epochTime);
+    my $expectedReturn = $testCase->{expectedReturn};
+
+    is($got, $expectedReturn, "$testNameStart return value.");
+}
diff --git a/Tools/Scripts/webkitperl/features.pm b/Tools/Scripts/webkitperl/features.pm
new file mode 100644
index 0000000..e546c39
--- /dev/null
+++ b/Tools/Scripts/webkitperl/features.pm
@@ -0,0 +1,96 @@
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2012 Apple Inc. All rights reserved
+# Copyright (C) 2006 Alexey Proskuryakov (ap@nypop.com)
+# Copyright (C) 2010 Andras Becsi (abecsi@inf.u-szeged.hu), University of Szeged
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+#     its contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Module to share code to detect the existance of features in built binaries.
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib $FindBin::Bin;
+use webkitdirs;
+
+BEGIN {
+   use Exporter   ();
+   our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
+   $VERSION     = 1.00;
+   @ISA         = qw(Exporter);
+   @EXPORT      = qw(&checkWebCoreFeatureSupport);
+   %EXPORT_TAGS = ( );
+   @EXPORT_OK   = ();
+}
+
+sub libraryContainsSymbol($$)
+{
+    my ($path, $symbol) = @_;
+
+    if (isCygwin() or isWindows()) {
+        # FIXME: Implement this for Windows.
+        return 0;
+    }
+
+    my $foundSymbol = 0;
+    if (-e $path) {
+        open NM, "-|", nmPath(), $path or die;
+        while (<NM>) {
+            $foundSymbol = 1 if /$symbol/; # FIXME: This should probably check for word boundaries before/after the symbol name.
+        }
+        close NM;
+    }
+    return $foundSymbol;
+}
+
+sub hasFeature($$)
+{
+    my ($featureName, $path) = @_;
+    my %symbolForFeature = (
+        "MathML" => "MathMLElement",
+        "SVG" => "SVGDefsElement", # We used to look for SVGElement but isSVGElement exists (and would match) in --no-svg builds.
+        "Accelerated Compositing" => "GraphicsLayer",
+        "3D Rendering" => "WebCoreHas3DRendering",
+        "3D Canvas" => "WebGLShader",
+        "MHTML" => "MHTMLArchive"
+    );
+    my $symbolName = $symbolForFeature{$featureName};
+    die "Unknown feature: $featureName" unless $symbolName;
+    return libraryContainsSymbol($path, $symbolName);
+}
+
+sub checkWebCoreFeatureSupport($$)
+{
+    my ($feature, $required) = @_;
+    my $libraryName = "WebCore";
+    my $path = builtDylibPathForName($libraryName);
+    my $hasFeature = hasFeature($feature, $path);
+    if ($required && !$hasFeature) {
+        die "$libraryName at \"$path\" does not include $hasFeature support.  See build-webkit --help\n";
+    }
+    return $hasFeature;
+}
+
+1;
diff --git a/Tools/Scripts/webkitperl/httpd.pm b/Tools/Scripts/webkitperl/httpd.pm
new file mode 100644
index 0000000..58ff108
--- /dev/null
+++ b/Tools/Scripts/webkitperl/httpd.pm
@@ -0,0 +1,335 @@
+# Copyright (C) 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights reserved
+# Copyright (C) 2006 Alexey Proskuryakov (ap@nypop.com)
+# Copyright (C) 2010 Andras Becsi (abecsi@inf.u-szeged.hu), University of Szeged
+# Copyright (C) 2011 Research In Motion Limited. 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.
+
+# Module to share code to start and stop the Apache daemon.
+
+use strict;
+use warnings;
+
+use File::Copy;
+use File::Path;
+use File::Spec;
+use File::Spec::Functions;
+use Fcntl ':flock';
+use IPC::Open2;
+
+use webkitdirs;
+
+BEGIN {
+   use Exporter   ();
+   our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
+   $VERSION     = 1.00;
+   @ISA         = qw(Exporter);
+   @EXPORT      = qw(&getHTTPDPath
+                     &hasHTTPD
+                     &getHTTPDConfigPathForTestDirectory
+                     &getDefaultConfigForTestDirectory
+                     &openHTTPD
+                     &closeHTTPD
+                     &setShouldWaitForUserInterrupt
+                     &waitForHTTPDLock
+                     &getWaitTime);
+   %EXPORT_TAGS = ( );
+   @EXPORT_OK   = ();
+}
+
+my $tmpDir = "/tmp";
+my $httpdLockPrefix = "WebKitHttpd.lock.";
+my $myLockFile;
+my $exclusiveLockFile = File::Spec->catfile($tmpDir, "WebKit.lock");
+my $httpdPidDir = File::Spec->catfile($tmpDir, "WebKit");
+my $httpdPidFile = File::Spec->catfile($httpdPidDir, "httpd.pid");
+my $httpdPid;
+my $waitForUserInterrupt = 0;
+my $waitBeginTime;
+my $waitEndTime;
+
+$SIG{'INT'} = 'handleInterrupt';
+$SIG{'TERM'} = 'handleInterrupt';
+
+sub getHTTPDPath
+{
+    my $httpdPath;
+    if (isDebianBased()) {
+        $httpdPath = "/usr/sbin/apache2";
+    } else {
+        $httpdPath = "/usr/sbin/httpd";
+    }
+    return $httpdPath;
+}
+
+sub hasHTTPD
+{
+    my @command = (getHTTPDPath(), "-v");
+    return system(@command) == 0;
+}
+
+sub getDefaultConfigForTestDirectory
+{
+    my ($testDirectory) = @_;
+    die "No test directory has been specified." unless ($testDirectory);
+
+    my $httpdConfig = getHTTPDConfigPathForTestDirectory($testDirectory);
+    my $documentRoot = "$testDirectory/http/tests";
+    my $jsTestResourcesDirectory = $testDirectory . "/fast/js/resources";
+    my $mediaResourcesDirectory = $testDirectory . "/media";
+    my $typesConfig = "$testDirectory/http/conf/mime.types";
+    my $httpdLockFile = File::Spec->catfile($httpdPidDir, "httpd.lock");
+    my $httpdScoreBoardFile = File::Spec->catfile($httpdPidDir, "httpd.scoreboard");
+
+    my @httpdArgs = (
+        "-f", "$httpdConfig",
+        "-C", "DocumentRoot \"$documentRoot\"",
+        # Setup a link to where the js test templates are stored, use -c so that mod_alias will already be loaded.
+        "-c", "Alias /js-test-resources \"$jsTestResourcesDirectory\"",
+        "-c", "Alias /media-resources \"$mediaResourcesDirectory\"",
+        "-c", "TypesConfig \"$typesConfig\"",
+        # Apache wouldn't run CGIs with permissions==700 otherwise
+        "-c", "User \"#$<\"",
+        "-c", "LockFile \"$httpdLockFile\"",
+        "-c", "PidFile \"$httpdPidFile\"",
+        "-c", "ScoreBoardFile \"$httpdScoreBoardFile\"",
+    );
+
+    # FIXME: Enable this on Windows once <rdar://problem/5345985> is fixed
+    # The version of Apache we use with Cygwin does not support SSL
+    my $sslCertificate = "$testDirectory/http/conf/webkit-httpd.pem";
+    push(@httpdArgs, "-c", "SSLCertificateFile \"$sslCertificate\"") unless isCygwin();
+
+    return @httpdArgs;
+
+}
+
+sub getHTTPDConfigPathForTestDirectory
+{
+    my ($testDirectory) = @_;
+    die "No test directory has been specified." unless ($testDirectory);
+
+    my $httpdConfig;
+    my $httpdPath = getHTTPDPath();
+    my $httpdConfDirectory = "$testDirectory/http/conf/";
+
+    if (isCygwin()) {
+        my $libPHP4DllPath = "/usr/lib/apache/libphp4.dll";
+        # FIXME: run-webkit-tests should not modify the user's system, especially not in this method!
+        unless (-x $libPHP4DllPath) {
+            copy("$httpdConfDirectory/libphp4.dll", $libPHP4DllPath);
+            chmod(0755, $libPHP4DllPath);
+        }
+        $httpdConfig = "cygwin-httpd.conf";  # This is an apache 1.3 config.
+    } elsif (isDebianBased()) {
+        $httpdConfig = "apache2-debian-httpd.conf";
+    } elsif (isFedoraBased()) {
+        $httpdConfig = "fedora-httpd.conf"; # This is an apache2 config, despite the name.
+    } else {
+        # All other ports use apache2, so just use our default apache2 config.
+        $httpdConfig = "apache2-httpd.conf";
+    }
+    return "$httpdConfDirectory/$httpdConfig";
+}
+
+sub openHTTPD(@)
+{
+    my (@args) = @_;
+    die "No HTTPD configuration has been specified" unless (@args);
+    mkdir($httpdPidDir, 0755);
+    die "No write permissions to $httpdPidDir" unless (-w $httpdPidDir);
+
+    if (-f $httpdPidFile) {
+        open (PIDFILE, $httpdPidFile);
+        my $oldPid = <PIDFILE>;
+        chomp $oldPid;
+        close PIDFILE;
+        if (0 != kill 0, $oldPid) {
+            print "\nhttpd is already running: pid $oldPid, killing...\n";
+            if (!killHTTPD($oldPid)) {
+                cleanUp();
+                die "Timed out waiting for httpd to quit";
+            }
+        }
+        unlink $httpdPidFile;
+    }
+
+    my $httpdPath = getHTTPDPath();
+
+    open2(">&1", \*HTTPDIN, $httpdPath, @args);
+
+    my $retryCount = 20;
+    while (!-f $httpdPidFile && $retryCount) {
+        sleep 1;
+        --$retryCount;
+    }
+
+    if (!$retryCount) {
+        cleanUp();
+        die "Timed out waiting for httpd to start";
+    }
+
+    $httpdPid = <PIDFILE> if open(PIDFILE, $httpdPidFile);
+    chomp $httpdPid if $httpdPid;
+    close PIDFILE;
+
+    waitpid($httpdPid, 0) if ($waitForUserInterrupt && $httpdPid);
+
+    return 1;
+}
+
+sub closeHTTPD
+{
+    close HTTPDIN;
+    my $succeeded = killHTTPD($httpdPid);
+    cleanUp();
+    unless ($succeeded) {
+        print STDERR "Timed out waiting for httpd to terminate!\n" unless $succeeded;
+        return 0;
+    }
+    return 1;
+}
+
+sub killHTTPD
+{
+    my ($pid) = @_;
+
+    return 1 unless $pid;
+
+    kill 15, $pid;
+
+    my $retryCount = 20;
+    while (kill(0, $pid) && $retryCount) {
+        sleep 1;
+        --$retryCount;
+    }
+    return $retryCount != 0;
+}
+
+sub setShouldWaitForUserInterrupt
+{
+    $waitForUserInterrupt = 1;
+}
+
+sub handleInterrupt
+{
+    # On Cygwin, when we receive a signal Apache is still running, so we need
+    # to kill it. On other platforms (at least Mac OS X), Apache will have
+    # already been killed, and trying to kill it again will cause us to hang.
+    # All we need to do in this case is clean up our own files.
+    if (isCygwin()) {
+        closeHTTPD();
+    } else {
+        cleanUp();
+    }
+
+    print "\n";
+    exit(1);
+}
+
+sub cleanUp
+{
+    rmdir $httpdPidDir;
+    unlink $exclusiveLockFile;
+    unlink $myLockFile if $myLockFile;
+}
+
+sub extractLockNumber
+{
+    my ($lockFile) = @_;
+    return -1 unless $lockFile;
+    return substr($lockFile, length($httpdLockPrefix));
+}
+
+sub getLockFiles
+{
+    opendir(TMPDIR, $tmpDir) or die "Could not open " . $tmpDir . ".";
+    my @lockFiles = grep {m/^$httpdLockPrefix\d+$/} readdir(TMPDIR);
+    @lockFiles = sort { extractLockNumber($a) <=> extractLockNumber($b) } @lockFiles;
+    closedir(TMPDIR);
+    return @lockFiles;
+}
+
+sub getNextAvailableLockNumber
+{
+    my @lockFiles = getLockFiles();
+    return 0 unless @lockFiles;
+    return extractLockNumber($lockFiles[-1]) + 1;
+}
+
+sub getLockNumberForCurrentRunning
+{
+    my @lockFiles = getLockFiles();
+    return 0 unless @lockFiles;
+    return extractLockNumber($lockFiles[0]);
+}
+
+sub waitForHTTPDLock
+{
+    $waitBeginTime = time;
+    scheduleHttpTesting();
+    # If we are the only one waiting for Apache just run the tests without any further checking
+    if (scalar getLockFiles() > 1) {
+        my $currentLockFile = File::Spec->catfile($tmpDir, "$httpdLockPrefix" . getLockNumberForCurrentRunning());
+        my $currentLockPid = <SCHEDULER_LOCK> if (-f $currentLockFile && open(SCHEDULER_LOCK, "<$currentLockFile"));
+        # Wait until we are allowed to run the http tests
+        while ($currentLockPid && $currentLockPid != $$) {
+            $currentLockFile = File::Spec->catfile($tmpDir, "$httpdLockPrefix" . getLockNumberForCurrentRunning());
+            if ($currentLockFile eq $myLockFile) {
+                $currentLockPid = <SCHEDULER_LOCK> if open(SCHEDULER_LOCK, "<$currentLockFile");
+                if ($currentLockPid != $$) {
+                    print STDERR "\nPID mismatch.\n";
+                    last;
+                }
+            } else {
+                sleep 1;
+            }
+        }
+    }
+    $waitEndTime = time;
+}
+
+sub scheduleHttpTesting
+{
+    # We need an exclusive lock file to avoid deadlocks and starvation and ensure that the scheduler lock numbers are sequential.
+    # The scheduler locks are used to schedule the running test sessions in first come first served order.
+    while (!(open(SEQUENTIAL_GUARD_LOCK, ">$exclusiveLockFile") && flock(SEQUENTIAL_GUARD_LOCK, LOCK_EX|LOCK_NB))) {}
+    $myLockFile = File::Spec->catfile($tmpDir, "$httpdLockPrefix" . getNextAvailableLockNumber());
+    open(SCHEDULER_LOCK, ">$myLockFile");
+    print SCHEDULER_LOCK "$$";
+    print SEQUENTIAL_GUARD_LOCK "$$";
+    close(SCHEDULER_LOCK);
+    close(SEQUENTIAL_GUARD_LOCK);
+    unlink $exclusiveLockFile;
+}
+
+sub getWaitTime
+{
+    my $waitTime = 0;
+    if ($waitBeginTime && $waitEndTime) {
+        $waitTime = $waitEndTime - $waitBeginTime;
+    }
+    return $waitTime;
+}
diff --git a/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/parser_unittests.pl b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/parser_unittests.pl
new file mode 100644
index 0000000..df29d47
--- /dev/null
+++ b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/parser_unittests.pl
@@ -0,0 +1,131 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) 2011 Google Inc.  All rights reserved.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Library General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Library General Public License for more details.
+#
+# You should have received a copy of the GNU Library General Public License
+# along with this library; see the file COPYING.LIB.  If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+
+# This script tests the parser of prepare-ChangeLog (i.e. get_function_line_ranges_for_XXXX()).
+# This script runs the unittests specified in @testFiles.
+
+use strict;
+use warnings;
+
+use Data::Dumper;
+use File::Basename;
+use File::Spec;
+use File::Temp qw(tempfile);
+use FindBin;
+use Getopt::Long;
+use Test::More;
+use lib File::Spec->catdir($FindBin::Bin, "..");
+use LoadAsModule qw(PrepareChangeLog prepare-ChangeLog);
+
+sub captureOutput($);
+sub convertAbsolutepathToWebKitPath($);
+
+my %testFiles = ("perl_unittests.pl" => "get_function_line_ranges_for_perl",
+                 "python_unittests.py" => "get_function_line_ranges_for_python",
+                 "java_unittests.java" => "get_function_line_ranges_for_java",
+                 "cpp_unittests.cpp" => "get_function_line_ranges_for_cpp",
+                 "javascript_unittests.js" => "get_function_line_ranges_for_javascript",
+                 "css_unittests.css" => "get_selector_line_ranges_for_css",
+                 "css_unittests_warning.css" => "get_selector_line_ranges_for_css",
+                );
+
+my $resetResults;
+GetOptions('reset-results' => \$resetResults);
+
+my @testSet;
+foreach my $testFile (sort keys %testFiles) {
+    my $basename = $testFile;
+    $basename = $1 if $basename =~ /^(.*)\.[^\.]*$/;
+    push @testSet, {method => $testFiles{$testFile},
+                    inputFile => File::Spec->catdir($FindBin::Bin, "resources", $testFile),
+                    expectedFile => File::Spec->catdir($FindBin::Bin, "resources", $basename . "-expected.txt")};
+}
+
+plan(tests => scalar @testSet);
+foreach my $test (@testSet) {
+    open FH, "< $test->{inputFile}" or die "Cannot open $test->{inputFile}: $!";
+    my $parser = eval "\\&PrepareChangeLog::$test->{method}";
+    my @ranges;
+    my ($stdout, $stderr) = captureOutput(sub { @ranges = $parser->(\*FH, $test->{inputFile}); });
+    close FH;
+    $stdout = convertAbsolutepathToWebKitPath($stdout);
+    $stderr = convertAbsolutepathToWebKitPath($stderr);
+
+    my %actualOutput = (ranges => \@ranges, stdout => $stdout, stderr => $stderr);
+    if ($resetResults) {
+        open FH, "> $test->{expectedFile}" or die "Cannot open $test->{expectedFile}: $!";
+        print FH Data::Dumper->new([\%actualOutput])->Terse(1)->Indent(1)->Dump();
+        close FH;
+        next;
+    }
+
+    open FH, "< $test->{expectedFile}" or die "Cannot open $test->{expectedFile}: $!";
+    local $/ = undef;
+    my $expectedOutput = eval <FH>;
+    close FH;
+
+    is_deeply(\%actualOutput, $expectedOutput, "Tests $test->{inputFile}");
+}
+
+sub captureOutput($)
+{
+    my ($targetMethod) = @_;
+
+    my ($stdoutFH, $stdoutFileName) = tempfile();
+    my ($stderrFH, $stderrFileName) = tempfile();
+
+    open OLDSTDOUT, ">&", \*STDOUT or die "Cannot dup STDOUT: $!";
+    open OLDSTDERR, ">&", \*STDERR or die "Cannot dup STDERR: $!";
+
+    open STDOUT, ">&", $stdoutFH or die "Cannot redirect STDOUT: $!";
+    open STDERR, ">&", $stderrFH or die "Cannot redirect STDERR: $!";
+
+    &$targetMethod();
+
+    close STDOUT;
+    close STDERR;
+
+    open STDOUT, ">&OLDSTDOUT" or die "Cannot dup OLDSTDOUT: $!";
+    open STDERR, ">&OLDSTDERR" or die "Cannot dup OLDSTDERR: $!";
+
+    close OLDSTDOUT;
+    close OLDSTDERR;
+
+    seek $stdoutFH, 0, 0;
+    seek $stderrFH, 0, 0;
+    local $/ = undef;
+    my $stdout = <$stdoutFH>;
+    my $stderr = <$stderrFH>;
+
+    close $stdoutFH;
+    close $stderrFH;
+
+    unlink $stdoutFileName or die "Cannot unlink $stdoutFileName: $!";
+    unlink $stderrFileName or die "Cannot unlink $stderrFileName: $!";
+    return ($stdout, $stderr);
+}
+
+sub convertAbsolutepathToWebKitPath($)
+{
+    my $string = shift;
+    my $sourceDir = LoadAsModule::sourceDir();
+    $sourceDir .= "/" unless $sourceDir =~ m-/$-;
+    $string =~ s/$sourceDir//g;
+    return $string;
+}
diff --git a/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/cpp_unittests-expected.txt b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/cpp_unittests-expected.txt
new file mode 100644
index 0000000..d94b074
--- /dev/null
+++ b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/cpp_unittests-expected.txt
@@ -0,0 +1,356 @@
+{
+  'stderr' => '',
+  'stdout' => '',
+  'ranges' => [
+    [
+      '20',
+      '22',
+      'func1'
+    ],
+    [
+      '24',
+      '27',
+      'func2'
+    ],
+    [
+      '29',
+      '29',
+      'func3'
+    ],
+    [
+      '33',
+      '36',
+      'func5'
+    ],
+    [
+      '44',
+      '46',
+      'func6'
+    ],
+    [
+      '52',
+      '54',
+      'func7'
+    ],
+    [
+      '58',
+      '60',
+      'func8'
+    ],
+    [
+      '62',
+      '64',
+      'func9'
+    ],
+    [
+      '67',
+      '69',
+      'func10'
+    ],
+    [
+      '76',
+      '78',
+      'func11'
+    ],
+    [
+      '85',
+      '87',
+      'func12'
+    ],
+    [
+      '89',
+      '91',
+      'func13'
+    ],
+    [
+      '93',
+      '97',
+      'func14'
+    ],
+    [
+      '99',
+      '102',
+      'func15'
+    ],
+    [
+      '104',
+      '106',
+      'funcOverloaded'
+    ],
+    [
+      '108',
+      '110',
+      'funcOverloaded'
+    ],
+    [
+      '112',
+      '114',
+      'funcOverloaded'
+    ],
+    [
+      '116',
+      '118',
+      'Class::func16'
+    ],
+    [
+      '120',
+      '122',
+      'Class1::Class2::func17'
+    ],
+    [
+      '124',
+      '126',
+      'Class2::func18'
+    ],
+    [
+      '128',
+      '130',
+      'Class2::func19'
+    ],
+    [
+      '132',
+      '134',
+      'Class2::func20'
+    ],
+    [
+      '136',
+      '138',
+      'Class2::func21'
+    ],
+    [
+      '140',
+      '142',
+      'Class2::func22'
+    ],
+    [
+      '144',
+      '146',
+      'func23'
+    ],
+    [
+      '149',
+      '151',
+      'func24'
+    ],
+    [
+      '153',
+      '155',
+      'Class2::func25'
+    ],
+    [
+      '158',
+      '159',
+      'Class1'
+    ],
+    [
+      '162',
+      '164',
+      'Class1::func26'
+    ],
+    [
+      '167',
+      '169',
+      'Class2::func27'
+    ],
+    [
+      '173',
+      '175',
+      'Class3::func28'
+    ],
+    [
+      '179',
+      '182',
+      'Class7::operator+'
+    ],
+    [
+      '185',
+      '187',
+      'Class100::Class100'
+    ],
+    [
+      '189',
+      '191',
+      'Class101::~Class101'
+    ],
+    [
+      '193',
+      '196',
+      'Class102::Class102'
+    ],
+    [
+      '198',
+      '201',
+      'Class103::Class103'
+    ],
+    [
+      '204',
+      '205',
+      'Struct1'
+    ],
+    [
+      '208',
+      '210',
+      'Struct1::func29'
+    ],
+    [
+      '213',
+      '215',
+      'Struct2::func30'
+    ],
+    [
+      '219',
+      '219',
+      'NameSpace1'
+    ],
+    [
+      '220',
+      '222',
+      'NameSpace1::func30'
+    ],
+    [
+      '223',
+      '223',
+      'NameSpace1'
+    ],
+    [
+      '228',
+      '228',
+      'NameSpace2'
+    ],
+    [
+      '229',
+      '231',
+      'NameSpace1::NameSpace2::func31'
+    ],
+    [
+      '232',
+      '232',
+      'NameSpace2'
+    ],
+    [
+      '237',
+      '240',
+      'Class104'
+    ],
+    [
+      '244',
+      '249',
+      'Class105'
+    ],
+    [
+      '253',
+      '254',
+      'Class106'
+    ],
+    [
+      '255',
+      '259',
+      'Class106::func32'
+    ],
+    [
+      '260',
+      '261',
+      'Class106'
+    ],
+    [
+      '262',
+      '266',
+      'Class106::func33'
+    ],
+    [
+      '267',
+      '268',
+      'Class106'
+    ],
+    [
+      '272',
+      '273',
+      'NameSpace3'
+    ],
+    [
+      '275',
+      '276',
+      'NameSpace4'
+    ],
+    [
+      '278',
+      '279',
+      'NameSpace3'
+    ],
+    [
+      '283',
+      '284',
+      'NameSpace5'
+    ],
+    [
+      '286',
+      '287',
+      'NameSpace6'
+    ],
+    [
+      '289',
+      '290',
+      'Class107'
+    ],
+    [
+      '291',
+      '295',
+      'NameSpace5::NameSpace6::Class107::func34'
+    ],
+    [
+      '296',
+      '297',
+      'Class107'
+    ],
+    [
+      '299',
+      '300',
+      'NameSpace6'
+    ],
+    [
+      '302',
+      '303',
+      'NameSpace5'
+    ],
+    [
+      '307',
+      '307',
+      'Class108'
+    ],
+    [
+      '308',
+      '320',
+      'Class108::func35'
+    ],
+    [
+      '321',
+      '321',
+      'Class108'
+    ],
+    [
+      '340',
+      '354',
+      'NameSpace7'
+    ],
+    [
+      '356',
+      '369',
+      'NameSpace8'
+    ],
+    [
+      '371',
+      '371',
+      'NameSpace7'
+    ],
+    [
+      '373',
+      '386',
+      'Class109'
+    ],
+    [
+      '388',
+      '388',
+      'NameSpace7'
+    ]
+  ]
+}
diff --git a/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/cpp_unittests.cpp b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/cpp_unittests.cpp
new file mode 100644
index 0000000..015a694
--- /dev/null
+++ b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/cpp_unittests.cpp
@@ -0,0 +1,389 @@
+/*
+ * Copyright (C) 2011 Google Inc.  All rights reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+void func1()
+{
+}
+
+void func2()
+{
+    return 123;
+}
+
+void func3() { return 123; }
+
+void func4();
+
+void func5()
+{
+    /* comment */
+}
+
+/*
+void funcInsideComment()
+{
+}
+*/
+
+void func6()
+{
+}
+
+#define MACRO 123 \
+    456 \
+    789
+
+void func7()
+{
+}
+
+#if 1 || 1 || \
+    1 || 1 || 1
+void func8()
+{
+}
+#else
+void func9()
+{
+}
+#endif
+
+void func10()
+{
+}
+
+std::string str = "abcde"
+"void funcInsideDoubleQuotedString()"
+"{"
+"}";
+
+void func11()
+{
+}
+
+std::string str = 'abcde'
+'void funcInsideSingleQuotedString()'
+'{'
+'}';
+
+void func12(int a)
+{
+}
+
+void func13(int a, int b, int c)
+{
+}
+
+void func14(int a, int b,
+            int c, int d
+            , int e, int f)
+{
+}
+
+void func15
+    (int a, int b)
+{
+}
+
+void funcOverloaded()
+{
+}
+
+void funcOverloaded(int a)
+{
+}
+
+void funcOverloaded(float a)
+{
+}
+
+void Class::func16()
+{
+}
+
+void Class1::Class2::func17()
+{
+}
+
+static void Class2::func18()
+{
+}
+
+inline void Class2::func19()
+{
+}
+
+const void Class2::func20()
+{
+}
+
+Class1::Type Class2::func21()
+{
+}
+
+inline static const Class1::Type Class2::func22()
+{
+}
+
+template<class T> void func23(T t)
+{
+}
+
+template<class T>
+void func24(T t)
+{
+}
+
+inline static Class1::Type Class2::func25()
+{
+}
+
+class Class1 {
+public:
+    void func26();
+};
+
+void Class1::func26()
+{
+}
+
+class Class2 {
+    void func27()
+    {
+    }
+};
+
+class Class3 : public Class4, Class5, Class6 {
+    void func28()
+    {
+    }
+};
+
+class Class7 {
+    int operator+()
+    {
+        return 123;
+    }
+};
+
+Class100::Class100()
+{
+}
+
+Class101::~Class101()
+{
+}
+
+Class102::Class102() :
+    member(1), member(2)
+{
+}
+
+Class103::Class103()
+    : member(1), member(2)
+{
+}
+
+struct Struct1 {
+public:
+    void func29();
+};
+
+void Struct1::func29()
+{
+}
+
+struct Struct2 {
+    void func30()
+    {
+    }
+};
+
+namespace NameSpace1 {
+
+void func30()
+{
+}
+
+}
+
+namespace NameSpace1 {
+namespace NameSpace2 {
+
+void func31()
+{
+}
+
+}
+}
+
+class Class104 {
+    int a;
+    int b;
+    int c;
+    int d;
+};
+
+class Class105 {
+public:
+    int a;
+    int b;
+private:
+    int c;
+    int d;
+};
+
+class Class106 {
+    int a;
+    int b;
+    void func32()
+    {
+        int c;
+        int d;
+    }
+    int e;
+    int f;
+    void func33()
+    {
+        int g;
+        int h;
+    }
+    int i;
+    int j;
+};
+
+namespace NameSpace3 {
+int a;
+int b;
+namespace NameSpace4 {
+int c;
+int d;
+};
+int e;
+int f;
+};
+
+namespace NameSpace5 {
+int a;
+int b;
+namespace NameSpace6 {
+int c;
+int d;
+class Class107 {
+    int e;
+    int f;
+    void func34()
+    {
+        int g;
+        int h;
+    }
+    int i;
+    int j;
+};
+int k;
+int ll;
+};
+int m;
+int n;
+};
+
+class Class108 {
+    int a;
+    void func35()
+    {
+        int b;
+        if (1) {
+            int c;
+            for (;;) {
+                int d;
+                int e;
+            }
+            int f;
+        }
+        int g;
+    }
+    int h;
+};
+
+int a[] = { };
+int a[] = {
+};
+int a[] = { 1, 2, 3 };
+int a[] = {
+    1,
+    2,
+    3
+};
+int a[3] = { 1, 2, 3 };
+int a[][3] = { {1, 2, 3}, {4, 5, 6} };
+int a[2][3] = { {1, 2, 3}, {4, 5, 6} };
+extern int a[];
+char a[4] = "test";
+
+namespace NameSpace7 {
+int a[] = { };
+int a[] = {
+};
+int a[] = { 1, 2, 3 };
+int a[] = {
+    1,
+    2,
+    3
+};
+int a[3] = { 1, 2, 3 };
+int a[][3] = { {1, 2, 3}, {4, 5, 6} };
+int a[2][3] = { {1, 2, 3}, {4, 5, 6} };
+extern int a[];
+char a[4] = "test";
+
+namespace NameSpace8 {
+int a[] = { };
+int a[] = {
+};
+int a[] = { 1, 2, 3 };
+int a[] = {
+    1,
+    2,
+    3
+};
+int a[3] = { 1, 2, 3 };
+int a[][3] = { {1, 2, 3}, {4, 5, 6} };
+int a[2][3] = { {1, 2, 3}, {4, 5, 6} };
+extern int a[];
+char a[4] = "test";
+};
+
+class Class109 {
+    int a[] = { };
+    int a[] = {
+    };
+    int a[] = { 1, 2, 3 };
+    int a[] = {
+        1,
+        2,
+        3
+    };
+    int a[3] = { 1, 2, 3 };
+    int a[][3] = { {1, 2, 3}, {4, 5, 6} };
+    int a[2][3] = { {1, 2, 3}, {4, 5, 6} };
+    extern int a[];
+    char a[4] = "test";
+};
+
+};
diff --git a/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests-expected.txt b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests-expected.txt
new file mode 100644
index 0000000..cacd783
--- /dev/null
+++ b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests-expected.txt
@@ -0,0 +1,96 @@
+{
+  'stderr' => '',
+  'stdout' => '',
+  'ranges' => [
+    [
+      '20',
+      '21',
+      'element1'
+    ],
+    [
+      '23',
+      '23',
+      'element2'
+    ],
+    [
+      '25',
+      '27',
+      'element3'
+    ],
+    [
+      '29',
+      '30',
+      'element4.p'
+    ],
+    [
+      '32',
+      '33',
+      'element5.p.q.r.s'
+    ],
+    [
+      '35',
+      '36',
+      'element6#p'
+    ],
+    [
+      '38',
+      '39',
+      'element7 element8'
+    ],
+    [
+      '41',
+      '42',
+      'element9.p element10.q'
+    ],
+    [
+      '44',
+      '45',
+      'element11#p element12#q'
+    ],
+    [
+      '47',
+      '48',
+      'element13, element14'
+    ],
+    [
+      '50',
+      '51',
+      '.p'
+    ],
+    [
+      '53',
+      '55',
+      '#p'
+    ],
+    [
+      '57',
+      '58',
+      '.p element15 #q element16.r element17#s'
+    ],
+    [
+      '60',
+      '61',
+      'element18:target'
+    ],
+    [
+      '63',
+      '65',
+      'element19'
+    ],
+    [
+      '67',
+      '69',
+      'element20'
+    ],
+    [
+      '71',
+      '74',
+      'element21'
+    ],
+    [
+      '76',
+      '79',
+      'element22'
+    ]
+  ]
+}
diff --git a/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests.css b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests.css
new file mode 100644
index 0000000..4bd172e
--- /dev/null
+++ b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests.css
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2011 Google Inc.  All rights reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+element1 {
+}
+
+element2 { }
+
+element3
+{
+}
+
+element4.p {
+}
+
+element5.p.q.r.s {
+}
+
+element6#p {
+}
+
+element7 element8 {
+}
+
+element9.p element10.q {
+}
+
+element11#p element12#q {
+}
+
+element13, element14 {
+}
+
+.p {
+}
+
+#p {
+
+}
+
+.p element15 #q element16.r element17#s {
+}
+
+element18:target {
+}
+
+element19 {
+    property1: 123
+}
+
+element20 {
+    property1: 123;
+}
+
+element21 {
+    property1: 123;
+    property2: 456;
+}
+
+element22 {
+    property1: 123;
+    /* comment */
+}
+
+/*
+elementInsideComment {
+    property1: 123;
+}
+*/
diff --git a/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests_warning-expected.txt b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests_warning-expected.txt
new file mode 100644
index 0000000..27a5399
--- /dev/null
+++ b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests_warning-expected.txt
@@ -0,0 +1,59 @@
+{
+  'stderr' => 'mismatched comment found in Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests_warning.css
+mismatched comment found in Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests_warning.css
+mismatched comment found in Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests_warning.css
+mismatched brace found in Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests_warning.css
+mismatched brace found in Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests_warning.css
+mismatched brace found in Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests_warning.css
+mismatched brace found in Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests_warning.css
+mismatched brace found in Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests_warning.css
+',
+  'stdout' => '',
+  'ranges' => [
+    [
+      '20',
+      '22',
+      'element1'
+    ],
+    [
+      '24',
+      '26',
+      'element2'
+    ],
+    [
+      '28',
+      '30',
+      'element3'
+    ],
+    [
+      '32',
+      '33',
+      'element4'
+    ],
+    [
+      0,
+      '34',
+      ''
+    ],
+    [
+      '36',
+      '38',
+      'element5'
+    ],
+    [
+      '40',
+      '41',
+      'element6'
+    ],
+    [
+      0,
+      '42',
+      ''
+    ],
+    [
+      0,
+      '47',
+      ''
+    ]
+  ]
+}
diff --git a/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests_warning.css b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests_warning.css
new file mode 100644
index 0000000..41b7b67
--- /dev/null
+++ b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/css_unittests_warning.css
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2011 Google Inc.  All rights reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+element1 {
+    /* comment mismatched */ */
+}
+
+element2 {
+    */ /* comment mismatched */
+}
+
+element3 {
+    */
+}
+
+element4 {
+    { } /* brace mismatched */
+}
+
+element5 {
+    { /* brace mismatched */
+}
+
+element6 {
+    } /* brace mismatched */
+}
+
+{
+element7 {
+    /* brace mismatched */
+}
diff --git a/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/java_unittests-expected.txt b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/java_unittests-expected.txt
new file mode 100644
index 0000000..6f37c67
--- /dev/null
+++ b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/java_unittests-expected.txt
@@ -0,0 +1,321 @@
+{
+  'stderr' => '',
+  'stdout' => '',
+  'ranges' => [
+    [
+      '20',
+      '26',
+      'Simple'
+    ],
+    [
+      '26',
+      '28',
+      'Simple.func1'
+    ],
+    [
+      '29',
+      '30',
+      'Simple'
+    ],
+    [
+      '30',
+      '33',
+      'Simple.func2'
+    ],
+    [
+      '34',
+      '34',
+      'Simple'
+    ],
+    [
+      '35',
+      '37',
+      'Simple.func3'
+    ],
+    [
+      '38',
+      '38',
+      'Simple'
+    ],
+    [
+      '39',
+      '39',
+      'Simple.func4'
+    ],
+    [
+      '40',
+      '41',
+      'Simple'
+    ],
+    [
+      '41',
+      '44',
+      'Simple.func5'
+    ],
+    [
+      '45',
+      '52',
+      'Simple'
+    ],
+    [
+      '52',
+      '54',
+      'Simple.func6'
+    ],
+    [
+      '55',
+      '56',
+      'Simple'
+    ],
+    [
+      '56',
+      '58',
+      'Simple.func7'
+    ],
+    [
+      '59',
+      '62',
+      'Simple'
+    ],
+    [
+      '60',
+      '64',
+      'Simple.func8'
+    ],
+    [
+      '65',
+      '67',
+      'Simple'
+    ],
+    [
+      '66',
+      '69',
+      'Simple.func9'
+    ],
+    [
+      '70',
+      '71',
+      'Simple'
+    ],
+    [
+      '71',
+      '73',
+      'Simple.func10'
+    ],
+    [
+      '74',
+      '75',
+      'Simple'
+    ],
+    [
+      '75',
+      '77',
+      'Simple.funcOverloaded'
+    ],
+    [
+      '78',
+      '79',
+      'Simple'
+    ],
+    [
+      '79',
+      '81',
+      'Simple.funcOverloaded'
+    ],
+    [
+      '82',
+      '83',
+      'Simple'
+    ],
+    [
+      '83',
+      '85',
+      'Simple.funcOverloaded'
+    ],
+    [
+      '86',
+      '87',
+      'Simple'
+    ],
+    [
+      '87',
+      '89',
+      'Simple.func11'
+    ],
+    [
+      '90',
+      '91',
+      'Simple'
+    ],
+    [
+      '91',
+      '93',
+      'Simple.func12'
+    ],
+    [
+      '94',
+      '95',
+      'Simple'
+    ],
+    [
+      '95',
+      '97',
+      'Simple.func13'
+    ],
+    [
+      '98',
+      '99',
+      'Simple'
+    ],
+    [
+      '99',
+      '101',
+      'Simple.func14'
+    ],
+    [
+      '102',
+      '103',
+      'Simple'
+    ],
+    [
+      '103',
+      '105',
+      'Simple.func15'
+    ],
+    [
+      '106',
+      '107',
+      'Simple'
+    ],
+    [
+      '107',
+      '109',
+      'Simple.func16'
+    ],
+    [
+      '110',
+      '111',
+      'Simple'
+    ],
+    [
+      '111',
+      '113',
+      'Simple.func17'
+    ],
+    [
+      '114',
+      '115',
+      'Simple'
+    ],
+    [
+      '115',
+      '117',
+      'Simple.func18'
+    ],
+    [
+      '118',
+      '119',
+      'Simple'
+    ],
+    [
+      '119',
+      '121',
+      'Simple.func19'
+    ],
+    [
+      '122',
+      '123',
+      'Simple'
+    ],
+    [
+      '123',
+      '125',
+      'Simple.func20'
+    ],
+    [
+      '126',
+      '127',
+      'Simple'
+    ],
+    [
+      '127',
+      '129',
+      'Simple.func21'
+    ],
+    [
+      '130',
+      '130',
+      'Simple'
+    ],
+    [
+      '135',
+      '137',
+      'Derived1'
+    ],
+    [
+      '137',
+      '139',
+      'Derived1.Derived1'
+    ],
+    [
+      '140',
+      '141',
+      'Derived1'
+    ],
+    [
+      '141',
+      '143',
+      'Derived1.func22'
+    ],
+    [
+      '144',
+      '144',
+      'Derived1'
+    ],
+    [
+      '146',
+      '148',
+      'Interface1'
+    ],
+    [
+      '150',
+      '154',
+      'Interface2'
+    ],
+    [
+      '154',
+      '156',
+      'Interface2.func23'
+    ],
+    [
+      '157',
+      '157',
+      'Interface2'
+    ],
+    [
+      '159',
+      '161',
+      'Derived2'
+    ],
+    [
+      '161',
+      '163',
+      'Derived2.Derived2'
+    ],
+    [
+      '164',
+      '165',
+      'Derived2'
+    ],
+    [
+      '165',
+      '167',
+      'Derived2.func23'
+    ],
+    [
+      '168',
+      '168',
+      'Derived2'
+    ]
+  ]
+}
diff --git a/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/java_unittests.java b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/java_unittests.java
new file mode 100644
index 0000000..ebee914
--- /dev/null
+++ b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/java_unittests.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2011 Google Inc.  All rights reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+public class Simple
+{
+    int a;
+    String b;
+    final int c = 12345;
+
+    void func1()
+    {
+    }
+
+    void func2()
+    {
+        return 123;
+    }
+
+    void func3() {
+        return 123;
+    }
+
+    void func4() { return 123; }
+
+    void func5()
+    {
+        /* comment */
+    }
+
+    /*
+      void funcInsideComment()
+      {
+      }
+    */
+
+    void func6(int a)
+    {
+    }
+
+    void func7(int a, int b, int c)
+    {
+    }
+
+    void func8(int a, int b,
+               int c, int d
+               , int e, int f)
+    {
+    }
+
+    void func9
+        (int a, int b)
+    {
+    }
+
+    LinkedList func10()
+    {
+    }
+
+    void funcOverloaded()
+    {
+    }
+
+    void funcOverloaded(int a)
+    {
+    }
+
+    void funcOverloaded(float a)
+    {
+    }
+
+    static void func11()
+    {
+    }
+
+    public void func12()
+    {
+    }
+
+    protected void func13()
+    {
+    }
+
+    private void func14()
+    {
+    }
+
+    static void func15()
+    {
+    }
+
+    final void func16()
+    {
+    }
+
+    abstract void func17()
+    {
+    }
+
+    synchronized void func18()
+    {
+    }
+
+    final static public synchronized void func19()
+    {
+    }
+
+    void func20() throws IOException
+    {
+    }
+
+    void func21() throws IOException, ArithmeticException
+    {
+    }
+}
+
+import java.util.*;
+import java.math.*;
+
+class Derived1 extends Base
+{
+    public Derived1()
+    {
+    }
+
+    public func22()
+    {
+    }
+}
+
+interface Interface1
+{
+}
+
+interface Interface2
+{
+    int a;
+
+    void func23()
+    {
+    }
+}
+
+class Derived2 extends Base interface Interface1, Interface2
+{
+    public Derived2()
+    {
+    }
+
+    public func23()
+    {
+    }
+}
diff --git a/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/javascript_unittests-expected.txt b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/javascript_unittests-expected.txt
new file mode 100644
index 0000000..35a3080
--- /dev/null
+++ b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/javascript_unittests-expected.txt
@@ -0,0 +1,136 @@
+{
+  'stderr' => '',
+  'stdout' => '',
+  'ranges' => [
+    [
+      '20',
+      '22',
+      'func1'
+    ],
+    [
+      '24',
+      '25',
+      'func2'
+    ],
+    [
+      '27',
+      '27',
+      'func3'
+    ],
+    [
+      '29',
+      '32',
+      'func4'
+    ],
+    [
+      '34',
+      '37',
+      'func5'
+    ],
+    [
+      '39',
+      '42',
+      'func6'
+    ],
+    [
+      '50',
+      '52',
+      'func7'
+    ],
+    [
+      '56',
+      '58',
+      'func8'
+    ],
+    [
+      '62',
+      '64',
+      'func9'
+    ],
+    [
+      '66',
+      '68',
+      'func10'
+    ],
+    [
+      '70',
+      '73',
+      'func11'
+    ],
+    [
+      '75',
+      '79',
+      'func12'
+    ],
+    [
+      '81',
+      '83',
+      'funcOverloaded'
+    ],
+    [
+      '85',
+      '87',
+      'funcOverloaded'
+    ],
+    [
+      '89',
+      '91',
+      'funcOverloaded'
+    ],
+    [
+      '94',
+      '96',
+      'Func1.prototype.get x1'
+    ],
+    [
+      '98',
+      '101',
+      'Func1.prototype.get x2'
+    ],
+    [
+      '103',
+      '105',
+      'Func1.prototype.set x1'
+    ],
+    [
+      '107',
+      '110',
+      'Func1.prototype.set x3'
+    ],
+    [
+      '114',
+      '116',
+      'Func2.prototype.func13'
+    ],
+    [
+      '118',
+      '120',
+      'Func2.prototype.func14'
+    ],
+    [
+      '122',
+      '125',
+      'Func2.prototype.func15'
+    ],
+    [
+      '133',
+      '134',
+      'func16.func17'
+    ],
+    [
+      '136',
+      '137',
+      'func16.func18'
+    ],
+    [
+      '139',
+      '141',
+      'func16.func19'
+    ],
+    [
+      '128',
+      '150',
+      'func16'
+    ]
+  ]
+}
diff --git a/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/javascript_unittests.js b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/javascript_unittests.js
new file mode 100644
index 0000000..93d1d4c
--- /dev/null
+++ b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/javascript_unittests.js
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2011 Google Inc.  All rights reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+function func1()
+{
+}
+
+function func2() {
+}
+
+function func3() { }
+
+function func4()
+{
+    return 123;
+}
+
+function func5()
+{
+    // comment
+}
+
+function func6()
+{
+    /* comment */
+}
+
+/*
+function funcInsideComment()
+{
+}
+*/
+
+function func7()
+{
+}
+
+var str1 = "function funcInsideDoubleQuotedString() {}";
+
+function func8()
+{
+}
+
+var str2 = 'function funcInsideSingleQuotedString() {}';
+
+function func9(a)
+{
+}
+
+function func10(a, b)
+{
+}
+
+function func11
+    (a, b)
+{
+}
+
+function func12(a, b,
+                c, d
+                , e, f)
+{
+}
+
+function funcOverloaded()
+{
+}
+
+function funcOverloaded(a)
+{
+}
+
+function funcOverloaded(a, b)
+{
+}
+
+Func1.prototype = {
+    get x1()
+    {
+    },
+
+    get x2()
+    {
+        return this.x2;
+    },
+
+    set x1(a)
+    {
+    },
+
+    set x3(a)
+    {
+        this.x3 = a;
+    }
+};
+
+Func2.prototype = {
+    func13 : function()
+    {
+    },
+
+    func14 : function(a)
+    {
+    },
+
+    func15 : function(a, b)
+    {
+        return 123;
+    }
+};
+
+function func16()
+{
+    var a = 123;
+    var b = 456;
+
+    var func17 = function() {
+    };
+
+    var func18 = function(a) {
+    };
+
+    var func19 = function(a, b) {
+        return 123;
+    };
+
+    func20(function()
+           {
+           },
+           function(a)
+           {
+               return 123;
+           });
+}
diff --git a/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/perl_unittests-expected.txt b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/perl_unittests-expected.txt
new file mode 100644
index 0000000..d5a2ce9
--- /dev/null
+++ b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/perl_unittests-expected.txt
@@ -0,0 +1,86 @@
+{
+  'stderr' => '',
+  'stdout' => '',
+  'ranges' => [
+    [
+      '21',
+      '23',
+      'func1'
+    ],
+    [
+      '25',
+      '28',
+      'func2'
+    ],
+    [
+      '30',
+      '34',
+      'func3'
+    ],
+    [
+      '36',
+      '38',
+      'func4'
+    ],
+    [
+      '40',
+      '42',
+      'func5'
+    ],
+    [
+      '44',
+      '46',
+      'func6'
+    ],
+    [
+      '48',
+      '53',
+      'func7'
+    ],
+    [
+      '55',
+      '60',
+      'func8'
+    ],
+    [
+      '62',
+      '67',
+      'func9'
+    ],
+    [
+      '69',
+      '76',
+      'func10'
+    ],
+    [
+      '78',
+      '88',
+      'func11'
+    ],
+    [
+      '90',
+      '100',
+      'func12'
+    ],
+    [
+      '102',
+      '111',
+      'func13'
+    ],
+    [
+      '113',
+      '118',
+      'func14'
+    ],
+    [
+      '120',
+      '125',
+      'func15'
+    ],
+    [
+      '127',
+      '128',
+      'func16'
+    ]
+  ]
+}
diff --git a/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/perl_unittests.pl b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/perl_unittests.pl
new file mode 100644
index 0000000..ed869f3
--- /dev/null
+++ b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/perl_unittests.pl
@@ -0,0 +1,143 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) 2011 Google Inc.  All rights reserved.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Library General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Library General Public License for more details.
+#
+# You should have received a copy of the GNU Library General Public License
+# along with this library; see the file COPYING.LIB.  If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+#
+
+sub func1
+{
+}
+
+sub func2
+{
+    return 123;
+}
+
+sub func3
+{
+    return 123;
+    return 456;
+}
+
+sub func4()
+{
+}
+
+sub func5($$$$)
+{
+}
+
+sub func6(\@\@\$\$\$\$)
+{
+}
+
+sub func7
+{
+    $str =<< EOF;
+
+EOF
+}
+
+sub func8
+{
+    $str =<< "EOF";
+
+EOF
+}
+
+sub func9
+{
+    $str =<< 'EOF';
+
+EOF
+}
+
+sub func10
+{
+    $str =<< EOF;
+sub funcInHereDocument1
+{
+}
+EOF
+}
+
+sub func11
+{
+    $str =<< EOF;
+sub funcInHereDocument2
+{
+}
+sub funcInHereDocument3
+{
+}
+EOF
+}
+
+sub func12
+{
+    $str =<< EOF;
+{
+{
+{
+}
+}
+}
+EOF
+}
+
+sub func13
+{
+    $str =<< EOF;
+
+$str << DUMMY_EOF
+
+DUMMY_EOF
+
+EOF
+}
+
+sub func14
+{
+    push(@array, << EOF);
+
+EOF
+}
+
+sub func15
+{
+    print << EOF;
+
+EOF
+}
+
+sub func16 {
+}
+
+sub prototypeDeclaration1;
+sub prototypeDeclaration2();
+sub prototypeDeclaration3(\@$$);
+
+if (1) {
+}
+
+for (@array) {
+}
+
+{}
+
+{
+}
diff --git a/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/python_unittests-expected.txt b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/python_unittests-expected.txt
new file mode 100644
index 0000000..8dad37e
--- /dev/null
+++ b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/python_unittests-expected.txt
@@ -0,0 +1,131 @@
+{
+  'stderr' => '',
+  'stdout' => '',
+  'ranges' => [
+    [
+      '21',
+      '23',
+      'func1'
+    ],
+    [
+      '24',
+      '27',
+      'func2'
+    ],
+    [
+      '28',
+      '34',
+      'func3'
+    ],
+    [
+      '35',
+      '38',
+      'func4'
+    ],
+    [
+      '39',
+      '42',
+      'func5'
+    ],
+    [
+      '43',
+      '47',
+      'func6'
+    ],
+    [
+      '48',
+      '51',
+      'funcOverloaded'
+    ],
+    [
+      '52',
+      '55',
+      'funcOverloaded'
+    ],
+    [
+      '56',
+      '59',
+      'funcOverloaded'
+    ],
+    [
+      '60',
+      '62',
+      'func7'
+    ],
+    [
+      '63',
+      '65',
+      'func7.func8'
+    ],
+    [
+      '66',
+      '67',
+      'func7.func8.func9'
+    ],
+    [
+      '68',
+      '68',
+      'func7.func8'
+    ],
+    [
+      '69',
+      '71',
+      'func7'
+    ],
+    [
+      '72',
+      '75',
+      'Class1'
+    ],
+    [
+      '76',
+      '78',
+      'Class2'
+    ],
+    [
+      '79',
+      '81',
+      'Class2.Class3'
+    ],
+    [
+      '82',
+      '83',
+      'Class2.Class3.Class4'
+    ],
+    [
+      '84',
+      '84',
+      'Class2.Class3'
+    ],
+    [
+      '85',
+      '87',
+      'Class2'
+    ],
+    [
+      '88',
+      '90',
+      'Class5'
+    ],
+    [
+      '91',
+      '92',
+      'Class5.func10'
+    ],
+    [
+      '93',
+      '94',
+      'Class5'
+    ],
+    [
+      '95',
+      '96',
+      'Class5.func11'
+    ],
+    [
+      '97',
+      '97',
+      'Class5'
+    ]
+  ]
+}
diff --git a/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/python_unittests.py b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/python_unittests.py
new file mode 100644
index 0000000..1a19cc2
--- /dev/null
+++ b/Tools/Scripts/webkitperl/prepare-ChangeLog_unittest/resources/python_unittests.py
@@ -0,0 +1,97 @@
+#
+# Copyright (C) 2011 Google Inc.  All rights reserved.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Library General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Library General Public License for more details.
+#
+# You should have received a copy of the GNU Library General Public License
+# along with this library; see the file COPYING.LIB.  If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+#
+
+
+def func1():
+
+
+def func2():
+    return
+
+
+def func3():
+    # comment
+    return
+
+# def funcInsideComment():
+
+
+def func4(a):
+    return
+
+
+def func5(a, b, c):
+    return
+
+
+def func6(a, b, \
+          c, d):
+    return
+
+
+def funcOverloaded(a):
+    return
+
+
+def funcOverloaded(a, b):
+    return
+
+
+def funcOverloaded(a, b, c=100):
+    return
+
+
+def func7():
+    pass
+
+    def func8():
+        pass
+
+        def func9():
+            pass
+        pass
+    pass
+
+
+class Class1:
+    pass
+
+
+class Class2:
+    pass
+
+    class Class3:
+        pass
+
+        class Class4:
+            pass
+        pass
+    pass
+
+
+class Class5:
+    pass
+
+    def func10():
+        pass
+    pass
+
+    def func11():
+        pass
+    pass
diff --git a/Tools/Scripts/webkitperl/run-leaks_unittest/run-leaks-report-v1.0.pl b/Tools/Scripts/webkitperl/run-leaks_unittest/run-leaks-report-v1.0.pl
new file mode 100644
index 0000000..3f315c3
--- /dev/null
+++ b/Tools/Scripts/webkitperl/run-leaks_unittest/run-leaks-report-v1.0.pl
@@ -0,0 +1,162 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2011 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.
+
+# tests run-leaks using original leaks report version 1.0
+
+use strict;
+use warnings;
+
+use File::Spec;
+use FindBin;
+use lib File::Spec->catdir($FindBin::Bin, "..");
+use Test::More;
+use LoadAsModule qw(RunLeaks run-leaks);
+
+my @input = split(/\n/, <<EOF);
+Process 1602: 86671 nodes malloced for 13261 KB
+Process 1602: 8 leaks for 160 total leaked bytes.
+Leak: 0x114d54708  size=24  zone: JavaScriptCore FastMalloc_0x7fff70a09d20
+	
+	0x18571798 0x00000001 0x00000000 0x00000000 	..W.............
+Leak: 0x1184b92b8  size=24  zone: JavaScriptCore FastMalloc_0x7fff70a09d20
+	
+	0x184b9048 0x00000001 0x00000000 0x00000000 	H.K.............
+Leak: 0x1184c84c8  size=24  zone: JavaScriptCore FastMalloc_0x7fff70a09d20
+	
+	0x1854e3d8 0x00000001 0x00000000 0x00000000 	..T.............
+Leak: 0x11854e3d8  size=24  zone: JavaScriptCore FastMalloc_0x7fff70a09d20
+	
+	0x1854e360 0x00000001 0x00000000 0x00000000 	`.T.............
+Leak: 0x118571798  size=24  zone: JavaScriptCore FastMalloc_0x7fff70a09d20
+	
+	0x184c84c8 0x00000001 0x00000000 0x00000000 	..L.............
+Leak: 0x11858b498  size=24  zone: JavaScriptCore FastMalloc_0x7fff70a09d20
+	
+	0x1858b4e0 0x00000001 0x00000000 0x00000000 	..X.............
+Leak: 0x118572530  size=8  zone: JavaScriptCore FastMalloc_0x7fff70a09d20
+	
+Leak: 0x118572538  size=8  zone: JavaScriptCore FastMalloc_0x7fff70a09d20
+	
+EOF
+
+my $expectedOutput =
+[
+  {
+    'leaksOutput' => join('', split(/\n/, <<EOF)),
+Leak: 0x114d54708  size=24  zone: JavaScriptCore FastMalloc_0x7fff70a09d20
+	
+	0x18571798 0x00000001 0x00000000 0x00000000 	..W.............
+EOF
+    'callStack' => '',
+    'address' => '0x114d54708',
+    'size' => '24',
+    'type' => '',
+  },
+  {
+    'leaksOutput' => join('', split(/\n/, <<EOF)),
+Leak: 0x1184b92b8  size=24  zone: JavaScriptCore FastMalloc_0x7fff70a09d20
+	
+	0x184b9048 0x00000001 0x00000000 0x00000000 	H.K.............
+EOF
+    'callStack' => '',
+    'address' => '0x1184b92b8',
+    'size' => '24',
+    'type' => '',
+  },
+
+  {
+    'leaksOutput' => join('', split(/\n/, <<EOF)),
+Leak: 0x1184c84c8  size=24  zone: JavaScriptCore FastMalloc_0x7fff70a09d20
+	
+	0x1854e3d8 0x00000001 0x00000000 0x00000000 	..T.............
+EOF
+    'callStack' => '',
+    'address' => '0x1184c84c8',
+    'size' => '24',
+    'type' => '',
+  },
+
+  {
+    'leaksOutput' => join('', split(/\n/, <<EOF)),
+Leak: 0x11854e3d8  size=24  zone: JavaScriptCore FastMalloc_0x7fff70a09d20
+	
+	0x1854e360 0x00000001 0x00000000 0x00000000 	`.T.............
+EOF
+    'callStack' => '',
+    'address' => '0x11854e3d8',
+    'size' => '24',
+    'type' => '',
+  },
+
+  {
+    'leaksOutput' => join('', split(/\n/, <<EOF)),
+Leak: 0x118571798  size=24  zone: JavaScriptCore FastMalloc_0x7fff70a09d20
+	
+	0x184c84c8 0x00000001 0x00000000 0x00000000 	..L.............
+EOF
+    'callStack' => '',
+    'address' => '0x118571798',
+    'size' => '24',
+    'type' => '',
+  },
+
+  {
+    'leaksOutput' => join('', split(/\n/, <<EOF)),
+Leak: 0x11858b498  size=24  zone: JavaScriptCore FastMalloc_0x7fff70a09d20
+	
+	0x1858b4e0 0x00000001 0x00000000 0x00000000 	..X.............
+EOF
+    'callStack' => '',
+    'address' => '0x11858b498',
+    'size' => '24',
+    'type' => '',
+  },
+
+  {
+    'leaksOutput' => join('', split(/\n/, <<EOF)),
+Leak: 0x118572530  size=8  zone: JavaScriptCore FastMalloc_0x7fff70a09d20
+	
+EOF
+    'callStack' => '',
+    'address' => '0x118572530',
+    'size' => '8',
+    'type' => '',
+  },
+
+  {
+    'leaksOutput' => join('', split(/\n/, <<EOF)),
+Leak: 0x118572538  size=8  zone: JavaScriptCore FastMalloc_0x7fff70a09d20
+	
+EOF
+    'callStack' => '',
+    'address' => '0x118572538',
+    'size' => '8',
+    'type' => '',
+  },
+];
+
+my $actualOutput = RunLeaks::parseLeaksOutput(@input);
+
+plan(tests => 1);
+is_deeply($actualOutput, $expectedOutput, "leaks Report Version 1.0 - no call stack");
diff --git a/Tools/Scripts/webkitperl/run-leaks_unittest/run-leaks-report-v2.0-new.pl b/Tools/Scripts/webkitperl/run-leaks_unittest/run-leaks-report-v2.0-new.pl
new file mode 100644
index 0000000..c4b6b2c
--- /dev/null
+++ b/Tools/Scripts/webkitperl/run-leaks_unittest/run-leaks-report-v2.0-new.pl
@@ -0,0 +1,126 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2011 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.
+
+# tests run-leaks using "new" leaks report version 2.0
+# - The "new" 2.0 format has "leaks Report Version:  2.0" after the two header sections.
+
+use strict;
+use warnings;
+
+use File::Spec;
+use FindBin;
+use lib File::Spec->catdir($FindBin::Bin, "..");
+use Test::More;
+use LoadAsModule qw(RunLeaks run-leaks);
+
+my @input = split(/\n/, <<EOF);
+Process:         DumpRenderTree [29903]
+Path:            /Volumes/Data/Build/Debug/DumpRenderTree
+Load Address:    0x102116000
+Identifier:      DumpRenderTree
+Version:         ??? (???)
+Code Type:       X86-64 (Native)
+Parent Process:  Python [29892]
+
+Date/Time:       2011-11-14 11:12:45.706 -0800
+OS Version:      Mac OS X 10.7.2 (11C74)
+Report Version:  7
+
+leaks Report Version:  2.0
+leaks(12871,0xacdfa2c0) malloc: process 89617 no longer exists, stack logs deleted from /tmp/stack-logs.89617.DumpRenderTree.A2giy6.index
+Process 29903: 60015 nodes malloced for 7290 KB
+Process 29903: 2 leaks for 1008 total leaked bytes.
+Leak: 0x7f9a3a612810  size=576  zone: DefaultMallocZone_0x10227b000   URLConnectionLoader::LoaderConnectionEventQueue  C++  CFNetwork
+	0x7f3af460 0x00007fff 0x7edf2f40 0x00007fff 	`.:.....@/.~....
+	0x7f3af488 0x00007fff 0xdab071b1 0x0000f068 	..:......q..h...
+	0x0100000a 0x00000000 0x7edf3f50 0x00007fff 	........P?.~....
+	0x00000000 0x00000000 0xdab071cc 0x0000f068 	.........q..h...
+	0x01000010 0x00000000 0x3a616210 0x00007f9a 	.........ba:....
+	0x00000000 0x00000000 0xdab071e5 0x0000f068 	.........q..h...
+	0x00000000 0x00000000 0x00000000 0x00000000 	................
+	0x00000000 0x00000000 0xdab07245 0x0000f068 	........Er..h...
+	...
+	Call stack: [thread 0x7fff7e3b4960]: | start | main DumpRenderTree.mm:835 | dumpRenderTree(int, char const**) DumpRenderTree.mm:794 | _ZL20runTestingServerLoopv DumpRenderTree.mm:744 | _ZL7runTestRKSs DumpRenderTree.mm:1273 | -[NSRunLoop(NSRunLoop) runMode:beforeDate:] | CFRunLoopRunSpecific | __CFRunLoopRun | __CFRunLoopDoSources0 | __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ | MultiplexerSource::perform() | URLConnectionClient::processEvents() | URLConnectionClient::ClientConnectionEventQueue::processAllEventsAndConsumePayload(XConnectionEventInfo<XClientEvent, XClientEventParams>*, long) | URLConnectionClient::_clientWillSendRequest(_CFURLRequest const*, _CFURLResponse*, URLConnectionClient::ClientConnectionEventQueue*) | URLConnectionClient::getRequestForTransmission(unsigned char, _CFURLResponse*, _CFURLRequest const*, __CFError**) | URLConnectionLoader::pushLoaderEvent(XConnectionEventInfo<XLoaderEvent, XLoaderEventParams>*) | CFAllocatedObject::operator new(unsigned long, __CFAllocator const*) | malloc_zone_malloc 
+Leak: 0x7f9a3a618090  size=432  zone: DefaultMallocZone_0x10227b000   URLConnectionInstanceData  CFType  CFNetwork
+	0x7edcab28 0x00007fff 0x00012b80 0x00000001 	(..~.....+......
+	0x7f3af310 0x00007fff 0x7f3af3f8 0x00007fff 	..:.......:.....
+	0x4d555458 0x00000000 0x00000000 0x00002068 	XTUM........h ..
+	0x00000000 0x00000000 0x00000c00 0x00000c00 	................
+	0x00000000 0x00000000 0x3a6180c8 0x00007f9a 	..........a:....
+	0x3a6180cc 0x00007f9a 0x00000000 0x00000000 	..a:............
+	0x7f3af418 0x00007fff 0x3a618060 0x00007f9a 	..:.....`.a:....
+	0x7f3af440 0x00007fff 0x00005813 0x00000001 	@.:......X......
+	...
+	Call stack: [thread 0x7fff7e3b4960]: | start | main DumpRenderTree.mm:835 | dumpRenderTree(int, char const**) DumpRenderTree.mm:794 | _ZL20runTestingServerLoopv DumpRenderTree.mm:744 | _ZL7runTestRKSs DumpRenderTree.mm:1273 | -[NSRunLoop(NSRunLoop) runMode:beforeDate:] | CFRunLoopRunSpecific | __CFRunLoopRun | __CFRunLoopDoTimer | __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ | _ZN7WebCoreL10timerFiredEP16__CFRunLoopTimerPv SharedTimerMac.mm:167 | WebCore::ThreadTimers::sharedTimerFired() ThreadTimers.cpp:94 | WebCore::ThreadTimers::sharedTimerFiredInternal() ThreadTimers.cpp:118 | WebCore::Timer<WebCore::DocumentLoader>::fired() Timer.h:100 | WebCore::DocumentLoader::substituteResourceDeliveryTimerFired(WebCore::Timer<WebCore::DocumentLoader>*) DocumentLoader.cpp:600 | WebCore::SubresourceLoader::didFinishLoading(double) SubresourceLoader.cpp:191 | WebCore::CachedResourceRequest::didFinishLoading(WebCore::SubresourceLoader*, double) CachedResourceRequest.cpp:196 | WebCore::CachedRawResource::data(WTF::PassRefPtr<WebCore::SharedBuffer>, bool) CachedRawResource.cpp:67 | WebCore::CachedResource::data(WTF::PassRefPtr<WebCore::SharedBuffer>, bool) CachedResource.cpp:166 | WebCore::CachedResource::checkNotify() CachedResource.cpp:156 | non-virtual thunk to WebCore::DocumentThreadableLoader::notifyFinished(WebCore::CachedResource*) | WebCore::DocumentThreadableLoader::notifyFinished(WebCore::CachedResource*) DocumentThreadableLoader.cpp:262 | WebCore::DocumentThreadableLoader::didFinishLoading(unsigned long, double) DocumentThreadableLoader.cpp:277 | non-virtual thunk to WebCore::XMLHttpRequest::didFinishLoading(unsigned long, double) | WebCore::XMLHttpRequest::didFinishLoading(unsigned long, double) XMLHttpRequest.cpp:1008 | WebCore::XMLHttpRequest::changeState(WebCore::XMLHttpRequest::State) XMLHttpRequest.cpp:329 | WebCore::XMLHttpRequest::callReadyStateChangeListener() XMLHttpRequest.cpp:345 | WebCore::XMLHttpRequestProgressEventThrottle::dispatchEvent(WTF::PassRefPtr<WebCore::Event>, WebCore::ProgressEventAction) XMLHttpRequestProgressEventThrottle.cpp:81 | WebCore::EventTarget::dispatchEvent(WTF::PassRefPtr<WebCore::Event>) EventTarget.cpp:176 | WebCore::EventTarget::fireEventListeners(WebCore::Event*) EventTarget.cpp:199 | WebCore::EventTarget::fireEventListeners(WebCore::Event*, WebCore::EventTargetData*, WTF::Vector<WebCore::RegisteredEventListener, 1ul>&) EventTarget.cpp:214 | WebCore::JSEventListener::handleEvent(WebCore::ScriptExecutionContext*, WebCore::Event*) JSEventListener.cpp:128 | WebCore::JSMainThreadExecState::call(JSC::ExecState*, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) JSMainThreadExecState.h:52 | JSC::call(JSC::ExecState*, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) CallData.cpp:39 | JSC::Interpreter::executeCall(JSC::ExecState*, JSC::JSObject*, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) Interpreter.cpp:986 | JSC::JITCode::execute(JSC::RegisterFile*, JSC::ExecState*, JSC::JSGlobalData*) JITCode.h:115 | 0x2298f4c011f8 | WebCore::jsXMLHttpRequestPrototypeFunctionSend(JSC::ExecState*) JSXMLHttpRequest.cpp:604 | WebCore::JSXMLHttpRequest::send(JSC::ExecState*) JSXMLHttpRequestCustom.cpp:132 | WebCore::XMLHttpRequest::send(WTF::String const&, int&) XMLHttpRequest.cpp:544 | WebCore::XMLHttpRequest::createRequest(int&) XMLHttpRequest.cpp:665 | WebCore::ThreadableLoader::create(WebCore::ScriptExecutionContext*, WebCore::ThreadableLoaderClient*, WebCore::ResourceRequest const&, WebCore::ThreadableLoaderOptions const&) ThreadableLoader.cpp:54 | WebCore::DocumentThreadableLoader::create(WebCore::Document*, WebCore::ThreadableLoaderClient*, WebCore::ResourceRequest const&, WebCore::ThreadableLoaderOptions const&) DocumentThreadableLoader.cpp:65 | WebCore::DocumentThreadableLoader::DocumentThreadableLoader(WebCore::Document*, WebCore::ThreadableLoaderClient*, WebCore::DocumentThreadableLoader::BlockingBehavior, WebCore::ResourceRequest const&, WebCore::ThreadableLoaderOptions const&) DocumentThreadableLoader.cpp:111 | WebCore::DocumentThreadableLoader::DocumentThreadableLoader(WebCore::Document*, WebCore::ThreadableLoaderClient*, WebCore::DocumentThreadableLoader::BlockingBehavior, WebCore::ResourceRequest const&, WebCore::ThreadableLoaderOptions const&) DocumentThreadableLoader.cpp:88 | WebCore::DocumentThreadableLoader::loadRequest(WebCore::ResourceRequest const&, WebCore::SecurityCheckPolicy) DocumentThreadableLoader.cpp:337 | WebCore::CachedResourceLoader::requestRawResource(WebCore::ResourceRequest&, WebCore::ResourceLoaderOptions const&) CachedResourceLoader.cpp:225 | WebCore::CachedResourceLoader::requestResource(WebCore::CachedResource::Type, WebCore::ResourceRequest&, WTF::String const&, WebCore::ResourceLoaderOptions const&, WebCore::ResourceLoadPriority, bool) CachedResourceLoader.cpp:400 | WebCore::CachedResourceLoader::loadResource(WebCore::CachedResource::Type, WebCore::ResourceRequest&, WTF::String const&, WebCore::ResourceLoadPriority, WebCore::ResourceLoaderOptions const&) CachedResourceLoader.cpp:469 | WebCore::CachedResource::load(WebCore::CachedResourceLoader*, WebCore::ResourceLoaderOptions const&) CachedResource.cpp:142 | WebCore::CachedResourceRequest::load(WebCore::CachedResourceLoader*, WebCore::CachedResource*, WebCore::ResourceLoaderOptions const&) CachedResourceRequest.cpp:135 | WebCore::ResourceLoadScheduler::scheduleSubresourceLoad(WebCore::Frame*, WebCore::SubresourceLoaderClient*, WebCore::ResourceRequest const&, WebCore::ResourceLoadPriority, WebCore::ResourceLoaderOptions const&) ResourceLoadScheduler.cpp:92 | WebCore::ResourceLoadScheduler::scheduleLoad(WebCore::ResourceLoader*, WebCore::ResourceLoadPriority) ResourceLoadScheduler.cpp:132 | WebCore::ResourceLoadScheduler::servePendingRequests(WebCore::ResourceLoadScheduler::HostInformation*, WebCore::ResourceLoadPriority) ResourceLoadScheduler.cpp:210 | WebCore::ResourceLoader::start() ResourceLoader.cpp:162 | WebCore::ResourceHandle::create(WebCore::NetworkingContext*, WebCore::ResourceRequest const&, WebCore::ResourceHandleClient*, bool, bool) ResourceHandle.cpp:71 | WebCore::ResourceHandle::start(WebCore::NetworkingContext*) ResourceHandleMac.mm:278 | WebCore::ResourceHandle::createNSURLConnection(objc_object*, bool, bool) ResourceHandleMac.mm:238 | -[NSURLConnection(NSURLConnectionPrivate) _initWithRequest:delegate:usesCache:maxContentLength:startImmediately:connectionProperties:] | CFURLConnectionCreateWithProperties | URLConnection::initialize(_CFURLRequest const*, CFURLConnectionClient_V1*, __CFDictionary const*) | CFObject::Allocate(unsigned long, CFClass const&, __CFAllocator const*) | _CFRuntimeCreateInstance | malloc_zone_malloc 
+EOF
+
+my $expectedOutput =
+[
+  {
+    'leaksOutput' => join('', split(/\n/, <<EOF)),
+Leak: 0x7f9a3a612810  size=576  zone: DefaultMallocZone_0x10227b000   URLConnectionLoader::LoaderConnectionEventQueue  C++  CFNetwork
+	0x7f3af460 0x00007fff 0x7edf2f40 0x00007fff 	`.:.....@/.~....
+	0x7f3af488 0x00007fff 0xdab071b1 0x0000f068 	..:......q..h...
+	0x0100000a 0x00000000 0x7edf3f50 0x00007fff 	........P?.~....
+	0x00000000 0x00000000 0xdab071cc 0x0000f068 	.........q..h...
+	0x01000010 0x00000000 0x3a616210 0x00007f9a 	.........ba:....
+	0x00000000 0x00000000 0xdab071e5 0x0000f068 	.........q..h...
+	0x00000000 0x00000000 0x00000000 0x00000000 	................
+	0x00000000 0x00000000 0xdab07245 0x0000f068 	........Er..h...
+	...
+	Call stack: [thread 0x7fff7e3b4960]: | start | main DumpRenderTree.mm:835 | dumpRenderTree(int, char const**) DumpRenderTree.mm:794 | _ZL20runTestingServerLoopv DumpRenderTree.mm:744 | _ZL7runTestRKSs DumpRenderTree.mm:1273 | -[NSRunLoop(NSRunLoop) runMode:beforeDate:] | CFRunLoopRunSpecific | __CFRunLoopRun | __CFRunLoopDoSources0 | __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ | MultiplexerSource::perform() | URLConnectionClient::processEvents() | URLConnectionClient::ClientConnectionEventQueue::processAllEventsAndConsumePayload(XConnectionEventInfo<XClientEvent, XClientEventParams>*, long) | URLConnectionClient::_clientWillSendRequest(_CFURLRequest const*, _CFURLResponse*, URLConnectionClient::ClientConnectionEventQueue*) | URLConnectionClient::getRequestForTransmission(unsigned char, _CFURLResponse*, _CFURLRequest const*, __CFError**) | URLConnectionLoader::pushLoaderEvent(XConnectionEventInfo<XLoaderEvent, XLoaderEventParams>*) | CFAllocatedObject::operator new(unsigned long, __CFAllocator const*) | malloc_zone_malloc 
+EOF
+    'callStack' => 
+'	Call stack: [thread 0x7fff7e3b4960]: | start | main DumpRenderTree.mm:835 | dumpRenderTree(int, char const**) DumpRenderTree.mm:794 | _ZL20runTestingServerLoopv DumpRenderTree.mm:744 | _ZL7runTestRKSs DumpRenderTree.mm:1273 | -[NSRunLoop(NSRunLoop) runMode:beforeDate:] | CFRunLoopRunSpecific | __CFRunLoopRun | __CFRunLoopDoSources0 | __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ | MultiplexerSource::perform() | URLConnectionClient::processEvents() | URLConnectionClient::ClientConnectionEventQueue::processAllEventsAndConsumePayload(XConnectionEventInfo<XClientEvent, XClientEventParams>*, long) | URLConnectionClient::_clientWillSendRequest(_CFURLRequest const*, _CFURLResponse*, URLConnectionClient::ClientConnectionEventQueue*) | URLConnectionClient::getRequestForTransmission(unsigned char, _CFURLResponse*, _CFURLRequest const*, __CFError**) | URLConnectionLoader::pushLoaderEvent(XConnectionEventInfo<XLoaderEvent, XLoaderEventParams>*) | CFAllocatedObject::operator new(unsigned long, __CFAllocator const*) | malloc_zone_malloc ',
+    'address' => '0x7f9a3a612810',
+    'size' => '576',
+    'type' => '',
+  },
+
+  {
+    'leaksOutput' => join('', split(/\n/, <<EOF)),
+Leak: 0x7f9a3a618090  size=432  zone: DefaultMallocZone_0x10227b000   URLConnectionInstanceData  CFType  CFNetwork
+	0x7edcab28 0x00007fff 0x00012b80 0x00000001 	(..~.....+......
+	0x7f3af310 0x00007fff 0x7f3af3f8 0x00007fff 	..:.......:.....
+	0x4d555458 0x00000000 0x00000000 0x00002068 	XTUM........h ..
+	0x00000000 0x00000000 0x00000c00 0x00000c00 	................
+	0x00000000 0x00000000 0x3a6180c8 0x00007f9a 	..........a:....
+	0x3a6180cc 0x00007f9a 0x00000000 0x00000000 	..a:............
+	0x7f3af418 0x00007fff 0x3a618060 0x00007f9a 	..:.....`.a:....
+	0x7f3af440 0x00007fff 0x00005813 0x00000001 	@.:......X......
+	...
+	Call stack: [thread 0x7fff7e3b4960]: | start | main DumpRenderTree.mm:835 | dumpRenderTree(int, char const**) DumpRenderTree.mm:794 | _ZL20runTestingServerLoopv DumpRenderTree.mm:744 | _ZL7runTestRKSs DumpRenderTree.mm:1273 | -[NSRunLoop(NSRunLoop) runMode:beforeDate:] | CFRunLoopRunSpecific | __CFRunLoopRun | __CFRunLoopDoTimer | __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ | _ZN7WebCoreL10timerFiredEP16__CFRunLoopTimerPv SharedTimerMac.mm:167 | WebCore::ThreadTimers::sharedTimerFired() ThreadTimers.cpp:94 | WebCore::ThreadTimers::sharedTimerFiredInternal() ThreadTimers.cpp:118 | WebCore::Timer<WebCore::DocumentLoader>::fired() Timer.h:100 | WebCore::DocumentLoader::substituteResourceDeliveryTimerFired(WebCore::Timer<WebCore::DocumentLoader>*) DocumentLoader.cpp:600 | WebCore::SubresourceLoader::didFinishLoading(double) SubresourceLoader.cpp:191 | WebCore::CachedResourceRequest::didFinishLoading(WebCore::SubresourceLoader*, double) CachedResourceRequest.cpp:196 | WebCore::CachedRawResource::data(WTF::PassRefPtr<WebCore::SharedBuffer>, bool) CachedRawResource.cpp:67 | WebCore::CachedResource::data(WTF::PassRefPtr<WebCore::SharedBuffer>, bool) CachedResource.cpp:166 | WebCore::CachedResource::checkNotify() CachedResource.cpp:156 | non-virtual thunk to WebCore::DocumentThreadableLoader::notifyFinished(WebCore::CachedResource*) | WebCore::DocumentThreadableLoader::notifyFinished(WebCore::CachedResource*) DocumentThreadableLoader.cpp:262 | WebCore::DocumentThreadableLoader::didFinishLoading(unsigned long, double) DocumentThreadableLoader.cpp:277 | non-virtual thunk to WebCore::XMLHttpRequest::didFinishLoading(unsigned long, double) | WebCore::XMLHttpRequest::didFinishLoading(unsigned long, double) XMLHttpRequest.cpp:1008 | WebCore::XMLHttpRequest::changeState(WebCore::XMLHttpRequest::State) XMLHttpRequest.cpp:329 | WebCore::XMLHttpRequest::callReadyStateChangeListener() XMLHttpRequest.cpp:345 | WebCore::XMLHttpRequestProgressEventThrottle::dispatchEvent(WTF::PassRefPtr<WebCore::Event>, WebCore::ProgressEventAction) XMLHttpRequestProgressEventThrottle.cpp:81 | WebCore::EventTarget::dispatchEvent(WTF::PassRefPtr<WebCore::Event>) EventTarget.cpp:176 | WebCore::EventTarget::fireEventListeners(WebCore::Event*) EventTarget.cpp:199 | WebCore::EventTarget::fireEventListeners(WebCore::Event*, WebCore::EventTargetData*, WTF::Vector<WebCore::RegisteredEventListener, 1ul>&) EventTarget.cpp:214 | WebCore::JSEventListener::handleEvent(WebCore::ScriptExecutionContext*, WebCore::Event*) JSEventListener.cpp:128 | WebCore::JSMainThreadExecState::call(JSC::ExecState*, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) JSMainThreadExecState.h:52 | JSC::call(JSC::ExecState*, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) CallData.cpp:39 | JSC::Interpreter::executeCall(JSC::ExecState*, JSC::JSObject*, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) Interpreter.cpp:986 | JSC::JITCode::execute(JSC::RegisterFile*, JSC::ExecState*, JSC::JSGlobalData*) JITCode.h:115 | 0x2298f4c011f8 | WebCore::jsXMLHttpRequestPrototypeFunctionSend(JSC::ExecState*) JSXMLHttpRequest.cpp:604 | WebCore::JSXMLHttpRequest::send(JSC::ExecState*) JSXMLHttpRequestCustom.cpp:132 | WebCore::XMLHttpRequest::send(WTF::String const&, int&) XMLHttpRequest.cpp:544 | WebCore::XMLHttpRequest::createRequest(int&) XMLHttpRequest.cpp:665 | WebCore::ThreadableLoader::create(WebCore::ScriptExecutionContext*, WebCore::ThreadableLoaderClient*, WebCore::ResourceRequest const&, WebCore::ThreadableLoaderOptions const&) ThreadableLoader.cpp:54 | WebCore::DocumentThreadableLoader::create(WebCore::Document*, WebCore::ThreadableLoaderClient*, WebCore::ResourceRequest const&, WebCore::ThreadableLoaderOptions const&) DocumentThreadableLoader.cpp:65 | WebCore::DocumentThreadableLoader::DocumentThreadableLoader(WebCore::Document*, WebCore::ThreadableLoaderClient*, WebCore::DocumentThreadableLoader::BlockingBehavior, WebCore::ResourceRequest const&, WebCore::ThreadableLoaderOptions const&) DocumentThreadableLoader.cpp:111 | WebCore::DocumentThreadableLoader::DocumentThreadableLoader(WebCore::Document*, WebCore::ThreadableLoaderClient*, WebCore::DocumentThreadableLoader::BlockingBehavior, WebCore::ResourceRequest const&, WebCore::ThreadableLoaderOptions const&) DocumentThreadableLoader.cpp:88 | WebCore::DocumentThreadableLoader::loadRequest(WebCore::ResourceRequest const&, WebCore::SecurityCheckPolicy) DocumentThreadableLoader.cpp:337 | WebCore::CachedResourceLoader::requestRawResource(WebCore::ResourceRequest&, WebCore::ResourceLoaderOptions const&) CachedResourceLoader.cpp:225 | WebCore::CachedResourceLoader::requestResource(WebCore::CachedResource::Type, WebCore::ResourceRequest&, WTF::String const&, WebCore::ResourceLoaderOptions const&, WebCore::ResourceLoadPriority, bool) CachedResourceLoader.cpp:400 | WebCore::CachedResourceLoader::loadResource(WebCore::CachedResource::Type, WebCore::ResourceRequest&, WTF::String const&, WebCore::ResourceLoadPriority, WebCore::ResourceLoaderOptions const&) CachedResourceLoader.cpp:469 | WebCore::CachedResource::load(WebCore::CachedResourceLoader*, WebCore::ResourceLoaderOptions const&) CachedResource.cpp:142 | WebCore::CachedResourceRequest::load(WebCore::CachedResourceLoader*, WebCore::CachedResource*, WebCore::ResourceLoaderOptions const&) CachedResourceRequest.cpp:135 | WebCore::ResourceLoadScheduler::scheduleSubresourceLoad(WebCore::Frame*, WebCore::SubresourceLoaderClient*, WebCore::ResourceRequest const&, WebCore::ResourceLoadPriority, WebCore::ResourceLoaderOptions const&) ResourceLoadScheduler.cpp:92 | WebCore::ResourceLoadScheduler::scheduleLoad(WebCore::ResourceLoader*, WebCore::ResourceLoadPriority) ResourceLoadScheduler.cpp:132 | WebCore::ResourceLoadScheduler::servePendingRequests(WebCore::ResourceLoadScheduler::HostInformation*, WebCore::ResourceLoadPriority) ResourceLoadScheduler.cpp:210 | WebCore::ResourceLoader::start() ResourceLoader.cpp:162 | WebCore::ResourceHandle::create(WebCore::NetworkingContext*, WebCore::ResourceRequest const&, WebCore::ResourceHandleClient*, bool, bool) ResourceHandle.cpp:71 | WebCore::ResourceHandle::start(WebCore::NetworkingContext*) ResourceHandleMac.mm:278 | WebCore::ResourceHandle::createNSURLConnection(objc_object*, bool, bool) ResourceHandleMac.mm:238 | -[NSURLConnection(NSURLConnectionPrivate) _initWithRequest:delegate:usesCache:maxContentLength:startImmediately:connectionProperties:] | CFURLConnectionCreateWithProperties | URLConnection::initialize(_CFURLRequest const*, CFURLConnectionClient_V1*, __CFDictionary const*) | CFObject::Allocate(unsigned long, CFClass const&, __CFAllocator const*) | _CFRuntimeCreateInstance | malloc_zone_malloc 
+EOF
+    'callStack' => 
+'	Call stack: [thread 0x7fff7e3b4960]: | start | main DumpRenderTree.mm:835 | dumpRenderTree(int, char const**) DumpRenderTree.mm:794 | _ZL20runTestingServerLoopv DumpRenderTree.mm:744 | _ZL7runTestRKSs DumpRenderTree.mm:1273 | -[NSRunLoop(NSRunLoop) runMode:beforeDate:] | CFRunLoopRunSpecific | __CFRunLoopRun | __CFRunLoopDoTimer | __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ | _ZN7WebCoreL10timerFiredEP16__CFRunLoopTimerPv SharedTimerMac.mm:167 | WebCore::ThreadTimers::sharedTimerFired() ThreadTimers.cpp:94 | WebCore::ThreadTimers::sharedTimerFiredInternal() ThreadTimers.cpp:118 | WebCore::Timer<WebCore::DocumentLoader>::fired() Timer.h:100 | WebCore::DocumentLoader::substituteResourceDeliveryTimerFired(WebCore::Timer<WebCore::DocumentLoader>*) DocumentLoader.cpp:600 | WebCore::SubresourceLoader::didFinishLoading(double) SubresourceLoader.cpp:191 | WebCore::CachedResourceRequest::didFinishLoading(WebCore::SubresourceLoader*, double) CachedResourceRequest.cpp:196 | WebCore::CachedRawResource::data(WTF::PassRefPtr<WebCore::SharedBuffer>, bool) CachedRawResource.cpp:67 | WebCore::CachedResource::data(WTF::PassRefPtr<WebCore::SharedBuffer>, bool) CachedResource.cpp:166 | WebCore::CachedResource::checkNotify() CachedResource.cpp:156 | non-virtual thunk to WebCore::DocumentThreadableLoader::notifyFinished(WebCore::CachedResource*) | WebCore::DocumentThreadableLoader::notifyFinished(WebCore::CachedResource*) DocumentThreadableLoader.cpp:262 | WebCore::DocumentThreadableLoader::didFinishLoading(unsigned long, double) DocumentThreadableLoader.cpp:277 | non-virtual thunk to WebCore::XMLHttpRequest::didFinishLoading(unsigned long, double) | WebCore::XMLHttpRequest::didFinishLoading(unsigned long, double) XMLHttpRequest.cpp:1008 | WebCore::XMLHttpRequest::changeState(WebCore::XMLHttpRequest::State) XMLHttpRequest.cpp:329 | WebCore::XMLHttpRequest::callReadyStateChangeListener() XMLHttpRequest.cpp:345 | WebCore::XMLHttpRequestProgressEventThrottle::dispatchEvent(WTF::PassRefPtr<WebCore::Event>, WebCore::ProgressEventAction) XMLHttpRequestProgressEventThrottle.cpp:81 | WebCore::EventTarget::dispatchEvent(WTF::PassRefPtr<WebCore::Event>) EventTarget.cpp:176 | WebCore::EventTarget::fireEventListeners(WebCore::Event*) EventTarget.cpp:199 | WebCore::EventTarget::fireEventListeners(WebCore::Event*, WebCore::EventTargetData*, WTF::Vector<WebCore::RegisteredEventListener, 1ul>&) EventTarget.cpp:214 | WebCore::JSEventListener::handleEvent(WebCore::ScriptExecutionContext*, WebCore::Event*) JSEventListener.cpp:128 | WebCore::JSMainThreadExecState::call(JSC::ExecState*, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) JSMainThreadExecState.h:52 | JSC::call(JSC::ExecState*, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) CallData.cpp:39 | JSC::Interpreter::executeCall(JSC::ExecState*, JSC::JSObject*, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) Interpreter.cpp:986 | JSC::JITCode::execute(JSC::RegisterFile*, JSC::ExecState*, JSC::JSGlobalData*) JITCode.h:115 | 0x2298f4c011f8 | WebCore::jsXMLHttpRequestPrototypeFunctionSend(JSC::ExecState*) JSXMLHttpRequest.cpp:604 | WebCore::JSXMLHttpRequest::send(JSC::ExecState*) JSXMLHttpRequestCustom.cpp:132 | WebCore::XMLHttpRequest::send(WTF::String const&, int&) XMLHttpRequest.cpp:544 | WebCore::XMLHttpRequest::createRequest(int&) XMLHttpRequest.cpp:665 | WebCore::ThreadableLoader::create(WebCore::ScriptExecutionContext*, WebCore::ThreadableLoaderClient*, WebCore::ResourceRequest const&, WebCore::ThreadableLoaderOptions const&) ThreadableLoader.cpp:54 | WebCore::DocumentThreadableLoader::create(WebCore::Document*, WebCore::ThreadableLoaderClient*, WebCore::ResourceRequest const&, WebCore::ThreadableLoaderOptions const&) DocumentThreadableLoader.cpp:65 | WebCore::DocumentThreadableLoader::DocumentThreadableLoader(WebCore::Document*, WebCore::ThreadableLoaderClient*, WebCore::DocumentThreadableLoader::BlockingBehavior, WebCore::ResourceRequest const&, WebCore::ThreadableLoaderOptions const&) DocumentThreadableLoader.cpp:111 | WebCore::DocumentThreadableLoader::DocumentThreadableLoader(WebCore::Document*, WebCore::ThreadableLoaderClient*, WebCore::DocumentThreadableLoader::BlockingBehavior, WebCore::ResourceRequest const&, WebCore::ThreadableLoaderOptions const&) DocumentThreadableLoader.cpp:88 | WebCore::DocumentThreadableLoader::loadRequest(WebCore::ResourceRequest const&, WebCore::SecurityCheckPolicy) DocumentThreadableLoader.cpp:337 | WebCore::CachedResourceLoader::requestRawResource(WebCore::ResourceRequest&, WebCore::ResourceLoaderOptions const&) CachedResourceLoader.cpp:225 | WebCore::CachedResourceLoader::requestResource(WebCore::CachedResource::Type, WebCore::ResourceRequest&, WTF::String const&, WebCore::ResourceLoaderOptions const&, WebCore::ResourceLoadPriority, bool) CachedResourceLoader.cpp:400 | WebCore::CachedResourceLoader::loadResource(WebCore::CachedResource::Type, WebCore::ResourceRequest&, WTF::String const&, WebCore::ResourceLoadPriority, WebCore::ResourceLoaderOptions const&) CachedResourceLoader.cpp:469 | WebCore::CachedResource::load(WebCore::CachedResourceLoader*, WebCore::ResourceLoaderOptions const&) CachedResource.cpp:142 | WebCore::CachedResourceRequest::load(WebCore::CachedResourceLoader*, WebCore::CachedResource*, WebCore::ResourceLoaderOptions const&) CachedResourceRequest.cpp:135 | WebCore::ResourceLoadScheduler::scheduleSubresourceLoad(WebCore::Frame*, WebCore::SubresourceLoaderClient*, WebCore::ResourceRequest const&, WebCore::ResourceLoadPriority, WebCore::ResourceLoaderOptions const&) ResourceLoadScheduler.cpp:92 | WebCore::ResourceLoadScheduler::scheduleLoad(WebCore::ResourceLoader*, WebCore::ResourceLoadPriority) ResourceLoadScheduler.cpp:132 | WebCore::ResourceLoadScheduler::servePendingRequests(WebCore::ResourceLoadScheduler::HostInformation*, WebCore::ResourceLoadPriority) ResourceLoadScheduler.cpp:210 | WebCore::ResourceLoader::start() ResourceLoader.cpp:162 | WebCore::ResourceHandle::create(WebCore::NetworkingContext*, WebCore::ResourceRequest const&, WebCore::ResourceHandleClient*, bool, bool) ResourceHandle.cpp:71 | WebCore::ResourceHandle::start(WebCore::NetworkingContext*) ResourceHandleMac.mm:278 | WebCore::ResourceHandle::createNSURLConnection(objc_object*, bool, bool) ResourceHandleMac.mm:238 | -[NSURLConnection(NSURLConnectionPrivate) _initWithRequest:delegate:usesCache:maxContentLength:startImmediately:connectionProperties:] | CFURLConnectionCreateWithProperties | URLConnection::initialize(_CFURLRequest const*, CFURLConnectionClient_V1*, __CFDictionary const*) | CFObject::Allocate(unsigned long, CFClass const&, __CFAllocator const*) | _CFRuntimeCreateInstance | malloc_zone_malloc ',
+    'address' => '0x7f9a3a618090',
+    'size' => '432',
+    'type' => '',
+  },
+];
+
+my $actualOutput = RunLeaks::parseLeaksOutput(@input);
+
+plan(tests => 1);
+is_deeply($actualOutput, $expectedOutput, "leaks Report Version 2.0 (new)");
diff --git a/Tools/Scripts/webkitperl/run-leaks_unittest/run-leaks-report-v2.0-old.pl b/Tools/Scripts/webkitperl/run-leaks_unittest/run-leaks-report-v2.0-old.pl
new file mode 100644
index 0000000..8e89220
--- /dev/null
+++ b/Tools/Scripts/webkitperl/run-leaks_unittest/run-leaks-report-v2.0-old.pl
@@ -0,0 +1,76 @@
+#!/usr/bin/perl -w
+
+# Copyright (C) 2011 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.
+
+# tests run-leaks using "old" leaks report version 2.0
+# - The "old" 2.0 format has "leaks Report Version:  2.0" at the top of the report.
+
+use strict;
+use warnings;
+
+use File::Spec;
+use FindBin;
+use lib File::Spec->catdir($FindBin::Bin, "..");
+use Test::More;
+use LoadAsModule qw(RunLeaks run-leaks);
+
+my @input = split(/\n/, <<EOF);
+leaks Report Version:  2.0
+Process:         Safari [53606]
+Path:            /Applications/Safari.app/Contents/MacOS/Safari
+Load Address:    0x100000000
+Identifier:      com.apple.Safari
+Version:         5.0 (6533.9)
+Build Info:      WebBrowser-75330900~1
+Code Type:       X86-64 (Native)
+Parent Process:  perl5.10.0 [53599]
+
+Date/Time:       2010-05-27 11:42:27.356 -0700
+OS Version:      Mac OS X 10.6.3 (10D571)
+Report Version:  6
+
+Process 53606: 112295 nodes malloced for 22367 KB
+Process 53606: 1 leak for 32 total leaked bytes.
+Leak: 0x1118c0e60  size=32  zone: DefaultMallocZone_0x105a92000	string 'com.apple.quarantine'
+	Call stack: [thread 0x7fff70126be0]: | 0x100001e84 | NSApplicationMain | +[NSBundle(NSNibLoading) loadNibNamed:owner:] | +[NSBundle(NSNibLoading) _loadNibFile:nameTable:withZone:ownerBundle:] | loadNib | -[NSIBObjectData nibInstantiateWithOwner:topLevelObjects:] | -[NSSet makeObjectsPerformSelector:] | 0x100003494 | 0x1001013ff | 0x10014dbb9 | 0x10014d923 | 0x10014d7d7 | 0x10014ccd9 | 0x100149c8e | 0x100149bd8 | xar_open | xar_file_unserialize | xar_prop_unserialize | xar_prop_unserialize | strdup | malloc | malloc_zone_malloc 
+EOF
+
+my $expectedOutput =
+[
+  {
+    'leaksOutput' => join('', split(/\n/, <<EOF)),
+Leak: 0x1118c0e60  size=32  zone: DefaultMallocZone_0x105a92000	string 'com.apple.quarantine'
+	Call stack: [thread 0x7fff70126be0]: | 0x100001e84 | NSApplicationMain | +[NSBundle(NSNibLoading) loadNibNamed:owner:] | +[NSBundle(NSNibLoading) _loadNibFile:nameTable:withZone:ownerBundle:] | loadNib | -[NSIBObjectData nibInstantiateWithOwner:topLevelObjects:] | -[NSSet makeObjectsPerformSelector:] | 0x100003494 | 0x1001013ff | 0x10014dbb9 | 0x10014d923 | 0x10014d7d7 | 0x10014ccd9 | 0x100149c8e | 0x100149bd8 | xar_open | xar_file_unserialize | xar_prop_unserialize | xar_prop_unserialize | strdup | malloc | malloc_zone_malloc 
+EOF
+    'callStack' => 
+'	Call stack: [thread 0x7fff70126be0]: | 0x100001e84 | NSApplicationMain | +[NSBundle(NSNibLoading) loadNibNamed:owner:] | +[NSBundle(NSNibLoading) _loadNibFile:nameTable:withZone:ownerBundle:] | loadNib | -[NSIBObjectData nibInstantiateWithOwner:topLevelObjects:] | -[NSSet makeObjectsPerformSelector:] | 0x100003494 | 0x1001013ff | 0x10014dbb9 | 0x10014d923 | 0x10014d7d7 | 0x10014ccd9 | 0x100149c8e | 0x100149bd8 | xar_open | xar_file_unserialize | xar_prop_unserialize | xar_prop_unserialize | strdup | malloc | malloc_zone_malloc ',
+    'address' => '0x1118c0e60',
+    'size' => '32',
+    'type' => 'com.apple.quarantine',
+  },
+];
+
+my $actualOutput = RunLeaks::parseLeaksOutput(@input);
+
+plan(tests => 1);
+is_deeply($actualOutput, $expectedOutput, "leaks Report Version 2.0 (old)");
diff --git a/Tools/Scripts/webkitpy/__init__.py b/Tools/Scripts/webkitpy/__init__.py
new file mode 100644
index 0000000..b376bf2
--- /dev/null
+++ b/Tools/Scripts/webkitpy/__init__.py
@@ -0,0 +1,13 @@
+# Required for Python to search this directory for module files
+
+# Keep this file free of any code or import statements that could
+# cause either an error to occur or a log message to be logged.
+# This ensures that calling code can import initialization code from
+# webkitpy before any errors or log messages due to code in this file.
+# Initialization code can include things like version-checking code and
+# logging configuration code.
+#
+# We do not execute any version-checking code or logging configuration
+# code in this file so that callers can opt-in as they want.  This also
+# allows different callers to choose different initialization code,
+# as necessary.
diff --git a/Tools/Scripts/webkitpy/bindings/__init__.py b/Tools/Scripts/webkitpy/bindings/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/bindings/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/bindings/main.py b/Tools/Scripts/webkitpy/bindings/main.py
new file mode 100644
index 0000000..15884bb
--- /dev/null
+++ b/Tools/Scripts/webkitpy/bindings/main.py
@@ -0,0 +1,173 @@
+#!/usr/bin/python
+# Copyright (C) 2011 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:
+# 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 COMPUTER, INC. ``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 COMPUTER, INC. OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+import os
+import os.path
+import shutil
+import subprocess
+import sys
+import tempfile
+from webkitpy.common.checkout.scm.detection import detect_scm_system
+from webkitpy.common.system.executive import ScriptError
+
+
+class BindingsTests:
+
+    def __init__(self, reset_results, generators, executive):
+        self.reset_results = reset_results
+        self.generators = generators
+        self.executive = executive
+
+    def generate_from_idl(self, generator, idl_file, output_directory, supplemental_dependency_file):
+        cmd = ['perl', '-w',
+               '-IWebCore/bindings/scripts',
+               'WebCore/bindings/scripts/generate-bindings.pl',
+               # idl include directories (path relative to generate-bindings.pl)
+               '--include', '.',
+               '--defines', 'TESTING_%s' % generator,
+               '--generator', generator,
+               '--outputDir', output_directory,
+               '--supplementalDependencyFile', supplemental_dependency_file,
+               idl_file]
+
+        exit_code = 0
+        try:
+            output = self.executive.run_command(cmd)
+            if output:
+                print output
+        except ScriptError, e:
+            print e.output
+            exit_code = e.exit_code
+        return exit_code
+
+    def generate_supplemental_dependency(self, input_directory, supplemental_dependency_file):
+        idl_files_list = tempfile.mkstemp()
+        for input_file in os.listdir(input_directory):
+            (name, extension) = os.path.splitext(input_file)
+            if extension != '.idl':
+                continue
+            os.write(idl_files_list[0], os.path.join(input_directory, input_file) + "\n")
+        os.close(idl_files_list[0])
+
+        cmd = ['perl', '-w',
+               '-IWebCore/bindings/scripts',
+               'WebCore/bindings/scripts/preprocess-idls.pl',
+               '--idlFilesList', idl_files_list[1],
+               '--defines', '',
+               '--supplementalDependencyFile', supplemental_dependency_file,
+               '--idlAttributesFile', 'WebCore/bindings/scripts/IDLAttributes.txt']
+
+        exit_code = 0
+        try:
+            output = self.executive.run_command(cmd)
+            if output:
+                print output
+        except ScriptError, e:
+            print e.output
+            exit_code = e.exit_code
+        os.remove(idl_files_list[1])
+        return exit_code
+
+    def detect_changes(self, generator, work_directory, reference_directory):
+        changes_found = False
+        for output_file in os.listdir(work_directory):
+            cmd = ['diff',
+                   '-u',
+                   '-N',
+                   os.path.join(reference_directory, output_file),
+                   os.path.join(work_directory, output_file)]
+
+            exit_code = 0
+            try:
+                output = self.executive.run_command(cmd)
+            except ScriptError, e:
+                output = e.output
+                exit_code = e.exit_code
+
+            if exit_code or output:
+                print 'FAIL: (%s) %s' % (generator, output_file)
+                print output
+                changes_found = True
+            else:
+                print 'PASS: (%s) %s' % (generator, output_file)
+        return changes_found
+
+    def run_tests(self, generator, input_directory, reference_directory, supplemental_dependency_file):
+        work_directory = reference_directory
+
+        passed = True
+        for input_file in os.listdir(input_directory):
+            (name, extension) = os.path.splitext(input_file)
+            if extension != '.idl':
+                continue
+            # Generate output into the work directory (either the given one or a
+            # temp one if not reset_results is performed)
+            if not self.reset_results:
+                work_directory = tempfile.mkdtemp()
+
+            if self.generate_from_idl(generator,
+                                      os.path.join(input_directory, input_file),
+                                      work_directory,
+                                      supplemental_dependency_file):
+                passed = False
+
+            if self.reset_results:
+                print "Reset results: (%s) %s" % (generator, input_file)
+                continue
+
+            # Detect changes
+            if self.detect_changes(generator, work_directory, reference_directory):
+                passed = False
+            shutil.rmtree(work_directory)
+
+        return passed
+
+    def main(self):
+        current_scm = detect_scm_system(os.curdir)
+        os.chdir(os.path.join(current_scm.checkout_root, 'Source'))
+
+        all_tests_passed = True
+
+        input_directory = os.path.join('WebCore', 'bindings', 'scripts', 'test')
+        supplemental_dependency_file = tempfile.mkstemp()[1]
+        if self.generate_supplemental_dependency(input_directory, supplemental_dependency_file):
+            print 'Failed to generate a supplemental dependency file.'
+            os.remove(supplemental_dependency_file)
+            return -1
+
+        for generator in self.generators:
+            input_directory = os.path.join('WebCore', 'bindings', 'scripts', 'test')
+            reference_directory = os.path.join('WebCore', 'bindings', 'scripts', 'test', generator)
+            if not self.run_tests(generator, input_directory, reference_directory, supplemental_dependency_file):
+                all_tests_passed = False
+
+        os.remove(supplemental_dependency_file)
+        print ''
+        if all_tests_passed:
+            print 'All tests PASS!'
+            return 0
+        else:
+            print 'Some tests FAIL! (To update the reference files, execute "run-bindings-tests --reset-results")'
+            return -1
diff --git a/Tools/Scripts/webkitpy/common/__init__.py b/Tools/Scripts/webkitpy/common/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/common/checkout/__init__.py b/Tools/Scripts/webkitpy/common/checkout/__init__.py
new file mode 100644
index 0000000..f385ae4
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/__init__.py
@@ -0,0 +1,3 @@
+# Required for Python to search this directory for module files
+
+from .checkout import Checkout
diff --git a/Tools/Scripts/webkitpy/common/checkout/baselineoptimizer.py b/Tools/Scripts/webkitpy/common/checkout/baselineoptimizer.py
new file mode 100644
index 0000000..d2d53a5
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/baselineoptimizer.py
@@ -0,0 +1,274 @@
+# Copyright (C) 2011, 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 copy
+import logging
+
+
+_log = logging.getLogger(__name__)
+
+
+# Yes, it's a hypergraph.
+# FIXME: Should this function live with the ports somewhere?
+# Perhaps this should move onto PortFactory?
+def _baseline_search_hypergraph(host, port_names):
+    hypergraph = {}
+
+    # These edges in the hypergraph aren't visible on build.webkit.org,
+    # but they impose constraints on how we optimize baselines.
+    hypergraph.update(_VIRTUAL_PORTS)
+
+    # FIXME: Should we get this constant from somewhere?
+    fallback_path = ['LayoutTests']
+
+    port_factory = host.port_factory
+    for port_name in port_names:
+        port = port_factory.get(port_name)
+        webkit_base = port.webkit_base()
+        search_path = port.baseline_search_path()
+        if search_path:
+            hypergraph[port_name] = [host.filesystem.relpath(path, webkit_base) for path in search_path] + fallback_path
+    return hypergraph
+
+
+_VIRTUAL_PORTS = {
+    'mac-future': ['LayoutTests/platform/mac-future', 'LayoutTests/platform/mac', 'LayoutTests'],
+    'win-future': ['LayoutTests/platform/win-future', 'LayoutTests/platform/win', 'LayoutTests'],
+    'qt-unknown': ['LayoutTests/platform/qt-unknown', 'LayoutTests/platform/qt', 'LayoutTests'],
+}
+
+
+# FIXME: Should this function be somewhere more general?
+def _invert_dictionary(dictionary):
+    inverted_dictionary = {}
+    for key, value in dictionary.items():
+        if inverted_dictionary.get(value):
+            inverted_dictionary[value].append(key)
+        else:
+            inverted_dictionary[value] = [key]
+    return inverted_dictionary
+
+
+class BaselineOptimizer(object):
+    def __init__(self, host, port_names):
+        self._host = host
+        self._filesystem = self._host.filesystem
+        self._scm = self._host.scm()
+        self._hypergraph = _baseline_search_hypergraph(host, port_names)
+        self._directories = reduce(set.union, map(set, self._hypergraph.values()))
+
+    def read_results_by_directory(self, baseline_name):
+        results_by_directory = {}
+        for directory in self._directories:
+            path = self._filesystem.join(self._scm.checkout_root, directory, baseline_name)
+            if self._filesystem.exists(path):
+                results_by_directory[directory] = self._filesystem.sha1(path)
+        return results_by_directory
+
+    def _results_by_port_name(self, results_by_directory):
+        results_by_port_name = {}
+        for port_name, search_path in self._hypergraph.items():
+            for directory in search_path:
+                if directory in results_by_directory:
+                    results_by_port_name[port_name] = results_by_directory[directory]
+                    break
+        return results_by_port_name
+
+    def _most_specific_common_directory(self, port_names):
+        paths = [self._hypergraph[port_name] for port_name in port_names]
+        common_directories = reduce(set.intersection, map(set, paths))
+
+        def score(directory):
+            return sum([path.index(directory) for path in paths])
+
+        _, directory = sorted([(score(directory), directory) for directory in common_directories])[0]
+        return directory
+
+    def _filter_port_names_by_result(self, predicate, port_names_by_result):
+        filtered_port_names_by_result = {}
+        for result, port_names in port_names_by_result.items():
+            filtered_port_names = filter(predicate, port_names)
+            if filtered_port_names:
+                filtered_port_names_by_result[result] = filtered_port_names
+        return filtered_port_names_by_result
+
+    def _place_results_in_most_specific_common_directory(self, port_names_by_result, results_by_directory):
+        for result, port_names in port_names_by_result.items():
+            directory = self._most_specific_common_directory(port_names)
+            results_by_directory[directory] = result
+
+    def _find_optimal_result_placement(self, baseline_name):
+        results_by_directory = self.read_results_by_directory(baseline_name)
+        results_by_port_name = self._results_by_port_name(results_by_directory)
+        port_names_by_result = _invert_dictionary(results_by_port_name)
+
+        new_results_by_directory = self._optimize_by_most_specific_common_directory(results_by_directory, results_by_port_name, port_names_by_result)
+        if not new_results_by_directory:
+            new_results_by_directory = self._optimize_by_pushing_results_up(results_by_directory, results_by_port_name, port_names_by_result)
+
+        return results_by_directory, new_results_by_directory
+
+    def _optimize_by_most_specific_common_directory(self, results_by_directory, results_by_port_name, port_names_by_result):
+        new_results_by_directory = {}
+        unsatisfied_port_names_by_result = port_names_by_result
+        while unsatisfied_port_names_by_result:
+            self._place_results_in_most_specific_common_directory(unsatisfied_port_names_by_result, new_results_by_directory)
+            new_results_by_port_name = self._results_by_port_name(new_results_by_directory)
+
+            def is_unsatisfied(port_name):
+                return results_by_port_name[port_name] != new_results_by_port_name[port_name]
+
+            new_unsatisfied_port_names_by_result = self._filter_port_names_by_result(is_unsatisfied, port_names_by_result)
+
+            if len(new_unsatisfied_port_names_by_result.values()) >= len(unsatisfied_port_names_by_result.values()):
+                return {}  # Frowns. We do not appear to be converging.
+            unsatisfied_port_names_by_result = new_unsatisfied_port_names_by_result
+
+        return new_results_by_directory
+
+    def _optimize_by_pushing_results_up(self, results_by_directory, results_by_port_name, port_names_by_result):
+        try:
+            results_by_directory = results_by_directory
+            best_so_far = results_by_directory
+            while True:
+                new_results_by_directory = copy.copy(best_so_far)
+                for port_name in self._hypergraph.keys():
+                    fallback_path = self._hypergraph[port_name]
+                    current_index, current_directory = self._find_in_fallbackpath(fallback_path, results_by_port_name[port_name], best_so_far)
+                    current_result = results_by_port_name[port_name]
+                    for index in range(current_index + 1, len(fallback_path)):
+                        new_directory = fallback_path[index]
+                        if not new_directory in new_results_by_directory:
+                            new_results_by_directory[new_directory] = current_result
+                            if current_directory in new_results_by_directory:
+                                del new_results_by_directory[current_directory]
+                        elif new_results_by_directory[new_directory] == current_result:
+                            if current_directory in new_results_by_directory:
+                                del new_results_by_directory[current_directory]
+                        else:
+                            # The new_directory contains a different result, so stop trying to push results up.
+                            break
+
+                if len(new_results_by_directory) >= len(best_so_far):
+                    # We've failed to improve, so give up.
+                    break
+                best_so_far = new_results_by_directory
+
+            return best_so_far
+        except KeyError as e:
+            # FIXME: KeyErrors get raised if we're missing baselines. We should handle this better.
+            return {}
+
+    def _find_in_fallbackpath(self, fallback_path, current_result, results_by_directory):
+        for index, directory in enumerate(fallback_path):
+            if directory in results_by_directory and (results_by_directory[directory] == current_result):
+                return index, directory
+        assert False, "result %s not found in fallback_path %s, %s" % (current_result, fallback_path, results_by_directory)
+
+    def _filtered_results_by_port_name(self, results_by_directory):
+        results_by_port_name = self._results_by_port_name(results_by_directory)
+        for port_name in _VIRTUAL_PORTS.keys():
+            if port_name in results_by_port_name:
+                del results_by_port_name[port_name]
+        return results_by_port_name
+
+    def _platform(self, filename):
+        platform_dir = 'LayoutTests' + self._filesystem.sep + 'platform' + self._filesystem.sep
+        if filename.startswith(platform_dir):
+            return filename.replace(platform_dir, '').split(self._filesystem.sep)[0]
+        platform_dir = self._filesystem.join(self._scm.checkout_root, platform_dir)
+        if filename.startswith(platform_dir):
+            return filename.replace(platform_dir, '').split(self._filesystem.sep)[0]
+        return '(generic)'
+
+    def _move_baselines(self, baseline_name, results_by_directory, new_results_by_directory):
+        data_for_result = {}
+        for directory, result in results_by_directory.items():
+            if not result in data_for_result:
+                source = self._filesystem.join(self._scm.checkout_root, directory, baseline_name)
+                data_for_result[result] = self._filesystem.read_binary_file(source)
+
+        file_names = []
+        for directory, result in results_by_directory.items():
+            if new_results_by_directory.get(directory) != result:
+                file_names.append(self._filesystem.join(self._scm.checkout_root, directory, baseline_name))
+        if file_names:
+            _log.debug("    Deleting:")
+            for platform_dir in sorted(self._platform(filename) for filename in file_names):
+                _log.debug("      " + platform_dir)
+            self._scm.delete_list(file_names)
+        else:
+            _log.debug("    (Nothing to delete)")
+
+        file_names = []
+        for directory, result in new_results_by_directory.items():
+            if results_by_directory.get(directory) != result:
+                destination = self._filesystem.join(self._scm.checkout_root, directory, baseline_name)
+                self._filesystem.maybe_make_directory(self._filesystem.split(destination)[0])
+                self._filesystem.write_binary_file(destination, data_for_result[result])
+                file_names.append(destination)
+        if file_names:
+            _log.debug("    Adding:")
+            for platform_dir in sorted(self._platform(filename) for filename in file_names):
+                _log.debug("      " + platform_dir)
+            self._scm.add_list(file_names)
+        else:
+            _log.debug("    (Nothing to add)")
+
+    def directories_by_result(self, baseline_name):
+        results_by_directory = self.read_results_by_directory(baseline_name)
+        return _invert_dictionary(results_by_directory)
+
+    def write_by_directory(self, results_by_directory, writer, indent):
+        for path in sorted(results_by_directory):
+            writer("%s%s: %s" % (indent, self._platform(path), results_by_directory[path][0:6]))
+
+    def optimize(self, baseline_name):
+        basename = self._filesystem.basename(baseline_name)
+        results_by_directory, new_results_by_directory = self._find_optimal_result_placement(baseline_name)
+        self.new_results_by_directory = new_results_by_directory
+        if new_results_by_directory == results_by_directory:
+            if new_results_by_directory:
+                _log.debug("  %s: (already optimal)" % basename)
+                self.write_by_directory(results_by_directory, _log.debug, "    ")
+            else:
+                _log.debug("  %s: (no baselines found)" % basename)
+            return True
+        if self._filtered_results_by_port_name(results_by_directory) != self._filtered_results_by_port_name(new_results_by_directory):
+            _log.warning("  %s: optimization failed" % basename)
+            self.write_by_directory(results_by_directory, _log.warning, "      ")
+            return False
+
+        _log.debug("  %s:" % basename)
+        _log.debug("    Before: ")
+        self.write_by_directory(results_by_directory, _log.debug, "      ")
+        _log.debug("    After: ")
+        self.write_by_directory(new_results_by_directory, _log.debug, "      ")
+
+        self._move_baselines(baseline_name, results_by_directory, new_results_by_directory)
+        return True
diff --git a/Tools/Scripts/webkitpy/common/checkout/baselineoptimizer_unittest.py b/Tools/Scripts/webkitpy/common/checkout/baselineoptimizer_unittest.py
new file mode 100644
index 0000000..a5fd065
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/baselineoptimizer_unittest.py
@@ -0,0 +1,194 @@
+# Copyright (C) 2011 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 sys
+import unittest
+
+from webkitpy.common.checkout.baselineoptimizer import BaselineOptimizer
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.host_mock import MockHost
+
+
+class TestBaselineOptimizer(BaselineOptimizer):
+    def __init__(self, mock_results_by_directory):
+        host = MockHost()
+        BaselineOptimizer.__init__(self, host, host.port_factory.all_port_names())
+        self._mock_results_by_directory = mock_results_by_directory
+
+    # We override this method for testing so we don't have to construct an
+    # elaborate mock file system.
+    def read_results_by_directory(self, baseline_name):
+        return self._mock_results_by_directory
+
+    def _move_baselines(self, baseline_name, results_by_directory, new_results_by_directory):
+        self.new_results_by_directory = new_results_by_directory
+
+
+class BaselineOptimizerTest(unittest.TestCase):
+    def _assertOptimization(self, results_by_directory, expected_new_results_by_directory):
+        baseline_optimizer = TestBaselineOptimizer(results_by_directory)
+        self.assertTrue(baseline_optimizer.optimize('mock-baseline.png'))
+        self.assertEqual(baseline_optimizer.new_results_by_directory, expected_new_results_by_directory)
+
+    def _assertOptimizationFailed(self, results_by_directory):
+        baseline_optimizer = TestBaselineOptimizer(results_by_directory)
+        self.assertFalse(baseline_optimizer.optimize('mock-baseline.png'))
+
+    def test_move_baselines(self):
+        host = MockHost()
+        host.filesystem.write_binary_file('/mock-checkout/LayoutTests/platform/chromium-win/another/test-expected.txt', 'result A')
+        host.filesystem.write_binary_file('/mock-checkout/LayoutTests/platform/chromium-mac/another/test-expected.txt', 'result A')
+        host.filesystem.write_binary_file('/mock-checkout/LayoutTests/platform/chromium/another/test-expected.txt', 'result B')
+        baseline_optimizer = BaselineOptimizer(host, host.port_factory.all_port_names())
+        baseline_optimizer._move_baselines('another/test-expected.txt', {
+            'LayoutTests/platform/chromium-win': 'aaa',
+            'LayoutTests/platform/chromium-mac': 'aaa',
+            'LayoutTests/platform/chromium': 'bbb',
+        }, {
+            'LayoutTests/platform/chromium': 'aaa',
+        })
+        self.assertEqual(host.filesystem.read_binary_file('/mock-checkout/LayoutTests/platform/chromium/another/test-expected.txt'), 'result A')
+
+    def test_chromium_linux_redundant_with_win(self):
+        self._assertOptimization({
+            'LayoutTests/platform/chromium-win': '462d03b9c025db1b0392d7453310dbee5f9a9e74',
+            'LayoutTests/platform/chromium-linux': '462d03b9c025db1b0392d7453310dbee5f9a9e74',
+        }, {
+            'LayoutTests/platform/chromium-win': '462d03b9c025db1b0392d7453310dbee5f9a9e74',
+        })
+
+    def test_no_add_mac_future(self):
+        self._assertOptimization({
+            'LayoutTests/platform/mac': '29a1715a6470d5dd9486a142f609708de84cdac8',
+            'LayoutTests/platform/win-xp': '453e67177a75b2e79905154ece0efba6e5bfb65d',
+            'LayoutTests/platform/mac-lion': 'c43eaeb358f49d5e835236ae23b7e49d7f2b089f',
+            'LayoutTests/platform/chromium-mac': 'a9ba153c700a94ae1b206d8e4a75a621a89b4554',
+        }, {
+            'LayoutTests/platform/mac': '29a1715a6470d5dd9486a142f609708de84cdac8',
+            'LayoutTests/platform/win-xp': '453e67177a75b2e79905154ece0efba6e5bfb65d',
+            'LayoutTests/platform/mac-lion': 'c43eaeb358f49d5e835236ae23b7e49d7f2b089f',
+            'LayoutTests/platform/chromium-mac': 'a9ba153c700a94ae1b206d8e4a75a621a89b4554',
+        })
+
+    def test_chromium_covers_mac_win_linux(self):
+        self._assertOptimization({
+            'LayoutTests/platform/chromium-mac': '462d03b9c025db1b0392d7453310dbee5f9a9e74',
+            'LayoutTests/platform/chromium-win': '462d03b9c025db1b0392d7453310dbee5f9a9e74',
+            'LayoutTests/platform/chromium-linux': '462d03b9c025db1b0392d7453310dbee5f9a9e74',
+        }, {
+            'LayoutTests/platform/chromium': '462d03b9c025db1b0392d7453310dbee5f9a9e74',
+        })
+
+    def test_mac_future(self):
+        self._assertOptimization({
+            'LayoutTests/platform/mac-lion': '462d03b9c025db1b0392d7453310dbee5f9a9e74',
+        }, {
+            'LayoutTests/platform/mac-lion': '462d03b9c025db1b0392d7453310dbee5f9a9e74',
+        })
+
+    def test_qt_unknown(self):
+        self._assertOptimization({
+            'LayoutTests/platform/qt': '462d03b9c025db1b0392d7453310dbee5f9a9e74',
+        }, {
+            'LayoutTests/platform/qt': '462d03b9c025db1b0392d7453310dbee5f9a9e74',
+        })
+
+    def test_win_does_not_drop_to_win_7sp0(self):
+        self._assertOptimization({
+            'LayoutTests/platform/win': '1',
+            'LayoutTests/platform/mac': '2',
+            'LayoutTests/platform/gtk': '3',
+            'LayoutTests/platform/qt': '4',
+            'LayoutTests/platform/chromium': '5',
+        }, {
+            'LayoutTests/platform/win': '1',
+            'LayoutTests/platform/mac': '2',
+            'LayoutTests/platform/gtk': '3',
+            'LayoutTests/platform/qt': '4',
+            'LayoutTests/platform/chromium': '5',
+        })
+
+    def test_common_directory_includes_root(self):
+        # This test case checks that we don't throw an exception when we fail
+        # to optimize.
+        self._assertOptimizationFailed({
+            'LayoutTests/platform/gtk': 'e8608763f6241ddacdd5c1ef1973ba27177d0846',
+            'LayoutTests/platform/qt': 'bcbd457d545986b7abf1221655d722363079ac87',
+            'LayoutTests/platform/chromium-win': '3764ac11e1f9fbadd87a90a2e40278319190a0d3',
+            'LayoutTests/platform/mac': 'e8608763f6241ddacdd5c1ef1973ba27177d0846',
+        })
+
+        self._assertOptimization({
+            'LayoutTests/platform/chromium-win': '23a30302a6910f8a48b1007fa36f3e3158341834',
+            'LayoutTests': '9c876f8c3e4cc2aef9519a6c1174eb3432591127',
+            'LayoutTests/platform/chromium-mac': '23a30302a6910f8a48b1007fa36f3e3158341834',
+            'LayoutTests/platform/chromium': '1',
+        }, {
+            'LayoutTests/platform/chromium': '23a30302a6910f8a48b1007fa36f3e3158341834',
+            'LayoutTests': '9c876f8c3e4cc2aef9519a6c1174eb3432591127',
+        })
+
+    def test_complex_shadowing(self):
+        # This test relies on OS specific functionality, so it doesn't work on Windows.
+        # FIXME: What functionality does this rely on?  When can we remove this if?
+        if sys.platform == 'win32':
+            return
+        self._assertOptimization({
+            'LayoutTests/platform/chromium-win': '462d03b9c025db1b0392d7453310dbee5f9a9e74',
+            'LayoutTests/platform/mac': '5daa78e55f05d9f0d1bb1f32b0cd1bc3a01e9364',
+            'LayoutTests/platform/chromium-win-xp': '462d03b9c025db1b0392d7453310dbee5f9a9e74',
+            'LayoutTests/platform/mac-lion': '7ad045ece7c030e2283c5d21d9587be22bcba56e',
+            'LayoutTests/platform/chromium-win': 'f83af9732ce74f702b8c9c4a3d9a4c6636b8d3bd',
+            'LayoutTests/platform/win-xp': '5b1253ef4d5094530d5f1bc6cdb95c90b446bec7',
+            'LayoutTests/platform/chromium-linux': 'f52fcdde9e4be8bd5142171cd859230bd4471036',
+        }, {
+            'LayoutTests/platform/chromium-win': '462d03b9c025db1b0392d7453310dbee5f9a9e74',
+            'LayoutTests/platform/mac': '5daa78e55f05d9f0d1bb1f32b0cd1bc3a01e9364',
+            'LayoutTests/platform/chromium-win-xp': '462d03b9c025db1b0392d7453310dbee5f9a9e74',
+            'LayoutTests/platform/mac-lion': '7ad045ece7c030e2283c5d21d9587be22bcba56e',
+            'LayoutTests/platform/chromium-win': 'f83af9732ce74f702b8c9c4a3d9a4c6636b8d3bd',
+            'LayoutTests/platform/win-xp': '5b1253ef4d5094530d5f1bc6cdb95c90b446bec7',
+            'LayoutTests/platform/chromium-linux': 'f52fcdde9e4be8bd5142171cd859230bd4471036'
+        })
+
+    def test_virtual_ports_filtered(self):
+        self._assertOptimization({
+            'LayoutTests/platform/chromium-mac': '1',
+            'LayoutTests/platform/chromium-mac-snowleopard': '1',
+            'LayoutTests/platform/chromium-win': '2',
+            'LayoutTests/platform/gtk': '3',
+            'LayoutTests/platform/efl': '3',
+            'LayoutTests/platform/qt': '4',
+            'LayoutTests/platform/mac': '5',
+        }, {
+            'LayoutTests/platform/chromium-mac': '1',
+            'LayoutTests/platform/chromium-win': '2',
+            'LayoutTests': '3',
+            'LayoutTests/platform/qt': '4',
+            'LayoutTests/platform/mac': '5',
+        })
diff --git a/Tools/Scripts/webkitpy/common/checkout/changelog.py b/Tools/Scripts/webkitpy/common/checkout/changelog.py
new file mode 100644
index 0000000..ae7b71f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/changelog.py
@@ -0,0 +1,377 @@
+# Copyright (C) 2009, 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.
+#
+# WebKit's Python module for parsing and modifying ChangeLog files
+
+import codecs
+import fileinput # inplace file editing for set_reviewer_in_changelog
+import re
+import textwrap
+
+from webkitpy.common.config.committers import CommitterList
+from webkitpy.common.config.committers import Account
+import webkitpy.common.config.urls as config_urls
+from webkitpy.common.system.deprecated_logging import log
+
+
+# FIXME: parse_bug_id_from_changelog should not be a free function.
+# Parse the bug ID out of a Changelog message based on the format that is
+# used by prepare-ChangeLog
+def parse_bug_id_from_changelog(message):
+    if not message:
+        return None
+    match = re.search("^\s*" + config_urls.bug_url_short + "$", message, re.MULTILINE)
+    if match:
+        return int(match.group('bug_id'))
+    match = re.search("^\s*" + config_urls.bug_url_long + "$", message, re.MULTILINE)
+    if match:
+        return int(match.group('bug_id'))
+    # We weren't able to find a bug URL in the format used by prepare-ChangeLog. Fall back to the
+    # first bug URL found anywhere in the message.
+    return config_urls.parse_bug_id(message)
+
+
+class ChangeLogEntry(object):
+    # e.g. 2009-06-03  Eric Seidel  <eric@webkit.org>
+    date_line_regexp = r'^(?P<date>\d{4}-\d{2}-\d{2})\s+(?P<authors>(?P<name>[^<]+?)\s+<(?P<email>[^<>]+)>.*?)$'
+
+    # e.g. * Source/WebCore/page/EventHandler.cpp: Implement FooBarQuux.
+    touched_files_regexp = r'^\s*\*\s*(?P<file>[A-Za-z0-9_\-\./\\]+)\s*\:'
+
+    # e.g. Reviewed by Darin Adler.
+    # (Discard everything after the first period to match more invalid lines.)
+    reviewed_by_regexp = r'^\s*((\w+\s+)+and\s+)?(Review|Rubber(\s*|-)stamp)(s|ed)?\s+([a-z]+\s+)*?by\s+(?P<reviewer>.*?)[\.,]?\s*$'
+
+    reviewed_byless_regexp = r'^\s*((Review|Rubber(\s*|-)stamp)(s|ed)?|RS)(\s+|\s*=\s*)(?P<reviewer>([A-Z]\w+\s*)+)[\.,]?\s*$'
+
+    reviewer_name_noise_regexp = re.compile(r"""
+    (\s+((tweaked\s+)?and\s+)?(landed|committed|okayed)\s+by.+) # "landed by", "commented by", etc...
+    |(^(Reviewed\s+)?by\s+) # extra "Reviewed by" or "by"
+    |([(<]\s*[\w_\-\.]+@[\w_\-\.]+[>)]) # email addresses
+    |([(<](https?://?bugs.)webkit.org[^>)]+[>)]) # bug url
+    |("[^"]+") # wresler names like 'Sean/Shawn/Shaun' in 'Geoffrey "Sean/Shawn/Shaun" Garen'
+    |('[^']+') # wresler names like "The Belly" in "Sam 'The Belly' Weinig"
+    |((Mr|Ms|Dr|Mrs|Prof)\.(\s+|$))
+    """, re.IGNORECASE | re.VERBOSE)
+
+    reviewer_name_casesensitive_noise_regexp = re.compile(r"""
+    ((\s+|^)(and\s+)?([a-z-]+\s+){5,}by\s+) # e.g. "and given a good once-over by"
+    |(\(\s*(?!(and|[A-Z])).+\)) # any parenthesis that doesn't start with "and" or a capital letter
+    |(with(\s+[a-z-]+)+) # phrases with "with no hesitation" in "Sam Weinig with no hesitation"
+    """, re.VERBOSE)
+
+    reviewer_name_noise_needing_a_backreference_regexp = re.compile(r"""
+    (\S\S)\.(?:(\s.+|$)) # Text after the two word characters (don't match initials) and a period followed by a space.
+    """, re.IGNORECASE | re.VERBOSE)
+
+    nobody_regexp = re.compile(r"""(\s+|^)nobody(
+    ((,|\s+-)?\s+(\w+\s+)+fix.*) # e.g. nobody, build fix...
+    |(\s*\([^)]+\).*) # NOBODY (..)...
+    |$)""", re.IGNORECASE | re.VERBOSE)
+
+    # e.g. == Rolled over to ChangeLog-2011-02-16 ==
+    rolled_over_regexp = r'^== Rolled over to ChangeLog-\d{4}-\d{2}-\d{2} ==$'
+
+    # e.g. git-svn-id: http://svn.webkit.org/repository/webkit/trunk@96161 268f45cc-cd09-0410-ab3c-d52691b4dbfc
+    svn_id_regexp = r'git-svn-id: http://svn.webkit.org/repository/webkit/trunk@(?P<svnid>\d+) '
+
+    def __init__(self, contents, committer_list=CommitterList(), revision=None):
+        self._contents = contents
+        self._committer_list = committer_list
+        self._revision = revision
+        self._parse_entry()
+
+    @staticmethod
+    def _parse_reviewer_text(text):
+        match = re.search(ChangeLogEntry.reviewed_by_regexp, text, re.MULTILINE | re.IGNORECASE)
+        if not match:
+            # There are cases where people omit "by". We match it only if reviewer part looked nice
+            # in order to avoid matching random lines that start with Reviewed
+            match = re.search(ChangeLogEntry.reviewed_byless_regexp, text, re.MULTILINE | re.IGNORECASE)
+        if not match:
+            return None, None
+
+        reviewer_text = match.group("reviewer")
+
+        reviewer_text = ChangeLogEntry.nobody_regexp.sub('', reviewer_text)
+        reviewer_text = ChangeLogEntry.reviewer_name_noise_regexp.sub('', reviewer_text)
+        reviewer_text = ChangeLogEntry.reviewer_name_casesensitive_noise_regexp.sub('', reviewer_text)
+        reviewer_text = ChangeLogEntry.reviewer_name_noise_needing_a_backreference_regexp.sub(r'\1', reviewer_text)
+        reviewer_text = reviewer_text.replace('(', '').replace(')', '')
+        reviewer_text = re.sub(r'\s\s+|[,.]\s*$', ' ', reviewer_text).strip()
+        if not len(reviewer_text):
+            return None, None
+
+        reviewer_list = ChangeLogEntry._split_contributor_names(reviewer_text)
+
+        # Get rid of "reviewers" like "even though this is just a..." in "Reviewed by Sam Weinig, even though this is just a..."
+        # and "who wrote the original code" in "Noam Rosenthal, who wrote the original code"
+        reviewer_list = [reviewer for reviewer in reviewer_list if not re.match('^who\s|^([a-z]+(\s+|\.|$)){6,}$', reviewer)]
+
+        return reviewer_text, reviewer_list
+
+    @staticmethod
+    def _split_contributor_names(text):
+        return re.split(r'\s*(?:,(?:\s+and\s+|&)?|(?:^|\s+)and\s+|&&|[/+&])\s*', text)
+
+    def _fuzz_match_reviewers(self, reviewers_text_list):
+        if not reviewers_text_list:
+            return []
+        list_of_reviewers = [self._committer_list.contributors_by_fuzzy_match(reviewer)[0] for reviewer in reviewers_text_list]
+        # Flatten lists and get rid of any reviewers with more than one candidate.
+        return [reviewers[0] for reviewers in list_of_reviewers if len(reviewers) == 1]
+
+    @staticmethod
+    def _parse_author_name_and_email(author_name_and_email):
+        match = re.match(r'(?P<name>.+?)\s+<(?P<email>[^>]+)>', author_name_and_email)
+        return {'name': match.group("name"), 'email': match.group("email")}
+
+    @staticmethod
+    def _parse_author_text(text):
+        if not text:
+            return []
+        authors = ChangeLogEntry._split_contributor_names(text)
+        assert(authors and len(authors) >= 1)
+        return [ChangeLogEntry._parse_author_name_and_email(author) for author in authors]
+
+    def _parse_entry(self):
+        match = re.match(self.date_line_regexp, self._contents, re.MULTILINE)
+        if not match:
+            log("WARNING: Creating invalid ChangeLogEntry:\n%s" % self._contents)
+
+        # FIXME: group("name") does not seem to be Unicode?  Probably due to self._contents not being unicode.
+        self._author_text = match.group("authors") if match else None
+        self._authors = ChangeLogEntry._parse_author_text(self._author_text)
+
+        self._reviewer_text, self._reviewers_text_list = ChangeLogEntry._parse_reviewer_text(self._contents)
+        self._reviewers = self._fuzz_match_reviewers(self._reviewers_text_list)
+        self._author = self._committer_list.contributor_by_email(self.author_email()) or self._committer_list.contributor_by_name(self.author_name())
+
+        self._touched_files = re.findall(self.touched_files_regexp, self._contents, re.MULTILINE)
+
+    def author_text(self):
+        return self._author_text
+
+    def revision(self):
+        return self._revision
+
+    def author_name(self):
+        return self._authors[0]['name']
+
+    def author_email(self):
+        return self._authors[0]['email']
+
+    def author(self):
+        return self._author  # Might be None
+
+    def authors(self):
+        return self._authors
+
+    # FIXME: Eventually we would like to map reviwer names to reviewer objects.
+    # See https://bugs.webkit.org/show_bug.cgi?id=26533
+    def reviewer_text(self):
+        return self._reviewer_text
+
+    # Might be None, might also not be a Reviewer!
+    def reviewer(self):
+        return self._reviewers[0] if len(self._reviewers) > 0 else None
+
+    def reviewers(self):
+        return self._reviewers
+
+    def has_valid_reviewer(self):
+        if self._reviewers_text_list:
+            for reviewer in self._reviewers_text_list:
+                reviewer = self._committer_list.committer_by_name(reviewer)
+                if reviewer:
+                    return True
+        return bool(re.search("unreviewed", self._contents, re.IGNORECASE))
+
+    def contents(self):
+        return self._contents
+
+    def bug_id(self):
+        return parse_bug_id_from_changelog(self._contents)
+
+    def touched_files(self):
+        return self._touched_files
+
+
+# FIXME: Various methods on ChangeLog should move into ChangeLogEntry instead.
+class ChangeLog(object):
+
+    def __init__(self, path):
+        self.path = path
+
+    _changelog_indent = " " * 8
+
+    @staticmethod
+    def parse_latest_entry_from_file(changelog_file):
+        """changelog_file must be a file-like object which returns
+        unicode strings.  Use codecs.open or StringIO(unicode())
+        to pass file objects to this class."""
+        date_line_regexp = re.compile(ChangeLogEntry.date_line_regexp)
+        rolled_over_regexp = re.compile(ChangeLogEntry.rolled_over_regexp)
+        entry_lines = []
+        # The first line should be a date line.
+        first_line = changelog_file.readline()
+        assert(isinstance(first_line, unicode))
+        if not date_line_regexp.match(first_line):
+            return None
+        entry_lines.append(first_line)
+
+        for line in changelog_file:
+            # If we've hit the next entry, return.
+            if date_line_regexp.match(line) or rolled_over_regexp.match(line):
+                # Remove the extra newline at the end
+                return ChangeLogEntry(''.join(entry_lines[:-1]))
+            entry_lines.append(line)
+        return None # We never found a date line!
+
+    svn_blame_regexp = re.compile(r'^(\s*(?P<revision>\d+) [^ ]+)\s*(?P<line>.*?\n)')
+
+    @staticmethod
+    def _separate_revision_and_line(line):
+        match = ChangeLog.svn_blame_regexp.match(line)
+        if not match:
+            return None, line
+        return int(match.group('revision')), match.group('line')
+
+    @staticmethod
+    def parse_entries_from_file(changelog_file):
+        """changelog_file must be a file-like object which returns
+        unicode strings.  Use codecs.open or StringIO(unicode())
+        to pass file objects to this class."""
+        date_line_regexp = re.compile(ChangeLogEntry.date_line_regexp)
+        rolled_over_regexp = re.compile(ChangeLogEntry.rolled_over_regexp)
+
+        # The first line should be a date line.
+        revision, first_line = ChangeLog._separate_revision_and_line(changelog_file.readline())
+        assert(isinstance(first_line, unicode))
+        if not date_line_regexp.match(ChangeLog.svn_blame_regexp.sub('', first_line)):
+            raise StopIteration
+
+        entry_lines = [first_line]
+        revisions_in_entry = {revision: 1} if revision != None else None
+        for line in changelog_file:
+            if revisions_in_entry:
+                revision, line = ChangeLog._separate_revision_and_line(line)
+
+            if rolled_over_regexp.match(line):
+                break
+
+            if date_line_regexp.match(line):
+                most_probable_revision = max(revisions_in_entry, key=revisions_in_entry.__getitem__) if revisions_in_entry else None
+                # Remove the extra newline at the end
+                yield ChangeLogEntry(''.join(entry_lines[:-1]), revision=most_probable_revision)
+                entry_lines = []
+                revisions_in_entry = {revision: 0}
+
+            entry_lines.append(line)
+            if revisions_in_entry:
+                revisions_in_entry[revision] = revisions_in_entry.get(revision, 0) + 1
+
+        most_probable_revision = max(revisions_in_entry, key=revisions_in_entry.__getitem__) if revisions_in_entry else None
+        yield ChangeLogEntry(''.join(entry_lines[:-1]), revision=most_probable_revision)
+
+    def latest_entry(self):
+        # ChangeLog files are always UTF-8, we read them in as such to support Reviewers with unicode in their names.
+        changelog_file = codecs.open(self.path, "r", "utf-8")
+        try:
+            return self.parse_latest_entry_from_file(changelog_file)
+        finally:
+            changelog_file.close()
+
+    # _wrap_line and _wrap_lines exist to work around
+    # http://bugs.python.org/issue1859
+
+    def _wrap_line(self, line):
+        return textwrap.fill(line,
+                             width=70,
+                             initial_indent=self._changelog_indent,
+                             # Don't break urls which may be longer than width.
+                             break_long_words=False,
+                             subsequent_indent=self._changelog_indent)
+
+    # Workaround as suggested by guido in
+    # http://bugs.python.org/issue1859#msg60040
+
+    def _wrap_lines(self, message):
+        lines = [self._wrap_line(line) for line in message.splitlines()]
+        return "\n".join(lines)
+
+    def update_with_unreviewed_message(self, message):
+        first_boilerplate_line_regexp = re.compile(
+                "%sNeed a short description \(OOPS!\)\." % self._changelog_indent)
+        removing_boilerplate = False
+        # inplace=1 creates a backup file and re-directs stdout to the file
+        for line in fileinput.FileInput(self.path, inplace=1):
+            if first_boilerplate_line_regexp.search(line):
+                message_lines = self._wrap_lines(message)
+                print first_boilerplate_line_regexp.sub(message_lines, line),
+                # Remove all the ChangeLog boilerplate before the first changed
+                # file.
+                removing_boilerplate = True
+            elif removing_boilerplate:
+                if line.find('*') >= 0: # each changed file is preceded by a *
+                    removing_boilerplate = False
+
+            if not removing_boilerplate:
+                print line,
+
+    def set_reviewer(self, reviewer):
+        latest_entry = self.latest_entry()
+        latest_entry_contents = latest_entry.contents()
+        reviewer_text = latest_entry.reviewer()
+        found_nobody = re.search("NOBODY\s*\(OOPS!\)", latest_entry_contents, re.MULTILINE)
+
+        if not found_nobody and not reviewer_text:
+            bug_url_number_of_items = len(re.findall(config_urls.bug_url_long, latest_entry_contents, re.MULTILINE))
+            bug_url_number_of_items += len(re.findall(config_urls.bug_url_short, latest_entry_contents, re.MULTILINE))
+            for line in fileinput.FileInput(self.path, inplace=1):
+                found_bug_url = re.search(config_urls.bug_url_long, line)
+                if not found_bug_url:
+                    found_bug_url = re.search(config_urls.bug_url_short, line)
+                print line,
+                if found_bug_url:
+                    if bug_url_number_of_items == 1:
+                        print "\n        Reviewed by %s." % (reviewer.encode("utf-8"))
+                    bug_url_number_of_items -= 1
+        else:
+            # inplace=1 creates a backup file and re-directs stdout to the file
+            for line in fileinput.FileInput(self.path, inplace=1):
+                # Trailing comma suppresses printing newline
+                print line.replace("NOBODY (OOPS!)", reviewer.encode("utf-8")),
+
+    def set_short_description_and_bug_url(self, short_description, bug_url):
+        message = "%s\n%s%s" % (short_description, self._changelog_indent, bug_url)
+        bug_boilerplate = "%sNeed the bug URL (OOPS!).\n" % self._changelog_indent
+        for line in fileinput.FileInput(self.path, inplace=1):
+            line = line.replace("Need a short description (OOPS!).", message.encode("utf-8"))
+            if line != bug_boilerplate:
+                print line,
diff --git a/Tools/Scripts/webkitpy/common/checkout/changelog_unittest.py b/Tools/Scripts/webkitpy/common/checkout/changelog_unittest.py
new file mode 100644
index 0000000..9591744
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/changelog_unittest.py
@@ -0,0 +1,585 @@
+# Copyright (C) 2009 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 codecs
+import os
+import tempfile
+import unittest
+
+from StringIO import StringIO
+
+from webkitpy.common.checkout.changelog import *
+
+
+class ChangeLogTest(unittest.TestCase):
+
+    _example_entry = u'''2009-08-17  Peter Kasting  <pkasting@google.com>
+
+        Reviewed by Tor Arne Vestb\xf8.
+
+        https://bugs.webkit.org/show_bug.cgi?id=27323
+        Only add Cygwin to the path when it isn't already there.  This avoids
+        causing problems for people who purposefully have non-Cygwin versions of
+        executables like svn in front of the Cygwin ones in their paths.
+
+        * DumpRenderTree/win/DumpRenderTree.vcproj:
+        * DumpRenderTree/win/ImageDiff.vcproj:
+        * DumpRenderTree/win/TestNetscapePlugin/TestNetscapePlugin.vcproj:
+'''
+
+    _rolled_over_footer = '== Rolled over to ChangeLog-2009-06-16 =='
+
+    # More example text than we need.  Eventually we need to support parsing this all and write tests for the parsing.
+    _example_changelog = u"""2009-08-17  Tor Arne Vestb\xf8  <vestbo@webkit.org>
+
+        <http://webkit.org/b/28393> check-webkit-style: add check for use of std::max()/std::min() instead of MAX()/MIN()
+
+        Reviewed by David Levin.
+
+        * Scripts/modules/cpp_style.py:
+        (_ERROR_CATEGORIES): Added 'runtime/max_min_macros'.
+        (check_max_min_macros): Added.  Returns level 4 error when MAX()
+        and MIN() macros are used in header files and C++ source files.
+        (check_style): Added call to check_max_min_macros().
+        * Scripts/modules/cpp_style_unittest.py: Added unit tests.
+        (test_max_macro): Added.
+        (test_min_macro): Added.
+
+2009-08-16  David Kilzer  <ddkilzer@apple.com>
+
+        Backed out r47343 which was mistakenly committed
+
+        * Scripts/bugzilla-tool:
+        * Scripts/modules/scm.py:
+
+2009-06-18  Darin Adler  <darin@apple.com>
+
+        Rubber stamped by Mark Rowe.
+
+        * DumpRenderTree/mac/DumpRenderTreeWindow.mm:
+        (-[DumpRenderTreeWindow close]): Resolved crashes seen during regression
+        tests. The close method can be called on a window that's already closed
+        so we can't assert here.
+
+2011-11-04  Benjamin Poulain  <bpoulain@apple.com>
+
+        [Mac] ResourceRequest's nsURLRequest() does not differentiate null and empty URLs with CFNetwork
+        https://bugs.webkit.org/show_bug.cgi?id=71539
+
+        Reviewed by David Kilzer.
+
+        In order to have CFURL and NSURL to be consistent when both are used on Mac,
+        KURL::createCFURL() is changed to support empty URL values.
+
+        * This change log entry is made up to test _parse_entry:
+            * a list of things
+
+        * platform/cf/KURLCFNet.cpp:
+        (WebCore::createCFURLFromBuffer):
+        (WebCore::KURL::createCFURL):
+        * platform/mac/KURLMac.mm :
+        (WebCore::KURL::operator NSURL *):
+        (WebCore::KURL::createCFURL):
+        * WebCoreSupport/ChromeClientEfl.cpp:
+        (WebCore::ChromeClientEfl::closeWindowSoon): call new function and moves its
+        previous functionality there.
+        * ewk/ewk_private.h:
+        * ewk/ewk_view.cpp:
+
+2011-03-02  Carol Szabo  <carol.szabo@nokia.com>
+
+        Reviewed by David Hyatt  <hyatt@apple.com>
+
+        content property doesn't support quotes
+        https://bugs.webkit.org/show_bug.cgi?id=6503
+
+        Added full support for quotes as defined by CSS 2.1.
+
+        Tests: fast/css/content/content-quotes-01.html
+               fast/css/content/content-quotes-02.html
+               fast/css/content/content-quotes-03.html
+               fast/css/content/content-quotes-04.html
+               fast/css/content/content-quotes-05.html
+               fast/css/content/content-quotes-06.html
+
+2011-03-31  Brent Fulgham  <bfulgham@webkit.org>
+
+       Reviewed Adam Roben.
+
+       [WinCairo] Implement Missing drawWindowsBitmap method.
+       https://bugs.webkit.org/show_bug.cgi?id=57409
+
+2011-03-28  Dirk Pranke  <dpranke@chromium.org>
+
+       RS=Tony Chang.
+
+       r81977 moved FontPlatformData.h from
+       WebCore/platform/graphics/cocoa to platform/graphics. This
+       change updates the chromium build accordingly.
+
+       https://bugs.webkit.org/show_bug.cgi?id=57281
+
+       * platform/graphics/chromium/CrossProcessFontLoading.mm:
+
+2011-05-04  Alexis Menard  <alexis.menard@openbossa.org>
+
+       Unreviewed warning fix.
+
+       The variable is just used in the ASSERT macro. Let's use ASSERT_UNUSED to avoid
+       a warning in Release build.
+
+       * accessibility/AccessibilityRenderObject.cpp:
+       (WebCore::lastChildConsideringContinuation):
+
+2011-10-11  Antti Koivisto  <antti@apple.com>
+
+       Resolve regular and visited link style in a single pass
+       https://bugs.webkit.org/show_bug.cgi?id=69838
+
+       Reviewed by Darin Adler
+
+       We can simplify and speed up selector matching by removing the recursive matching done
+       to generate the style for the :visited pseudo selector. Both regular and visited link style
+       can be generated in a single pass through the style selector.
+
+== Rolled over to ChangeLog-2009-06-16 ==
+"""
+
+    def test_parse_bug_id_from_changelog(self):
+        commit_text = '''
+2011-03-23  Ojan Vafai  <ojan@chromium.org>
+
+        Add failing result for WebKit2. All tests that require
+        focus fail on WebKit2. See https://bugs.webkit.org/show_bug.cgi?id=56988.
+
+        * platform/mac-wk2/fast/css/pseudo-any-expected.txt: Added.
+
+        '''
+
+        self.assertEquals(56988, parse_bug_id_from_changelog(commit_text))
+
+        commit_text = '''
+2011-03-23  Ojan Vafai  <ojan@chromium.org>
+
+        Add failing result for WebKit2. All tests that require
+        focus fail on WebKit2. See https://bugs.webkit.org/show_bug.cgi?id=56988.
+        https://bugs.webkit.org/show_bug.cgi?id=12345
+
+        * platform/mac-wk2/fast/css/pseudo-any-expected.txt: Added.
+
+        '''
+
+        self.assertEquals(12345, parse_bug_id_from_changelog(commit_text))
+
+        commit_text = '''
+2011-03-31  Adam Roben  <aroben@apple.com>
+
+        Quote the executable path we pass to ::CreateProcessW
+
+        This will ensure that spaces in the path will be interpreted correctly.
+
+        Fixes <http://webkit.org/b/57569> Web process sometimes fails to launch when there are
+        spaces in its path
+
+        Reviewed by Steve Falkenburg.
+
+        * UIProcess/Launcher/win/ProcessLauncherWin.cpp:
+        (WebKit::ProcessLauncher::launchProcess): Surround the executable path in quotes.
+
+        '''
+
+        self.assertEquals(57569, parse_bug_id_from_changelog(commit_text))
+
+        commit_text = '''
+2011-03-29  Timothy Hatcher  <timothy@apple.com>
+
+        Update WebCore Localizable.strings to contain WebCore, WebKit/mac and WebKit2 strings.
+
+        https://webkit.org/b/57354
+
+        Reviewed by Sam Weinig.
+
+        * English.lproj/Localizable.strings: Updated.
+        * StringsNotToBeLocalized.txt: Removed. To hard to maintain in WebCore.
+        * platform/network/cf/LoaderRunLoopCF.h: Remove a single quote in an #error so
+        extract-localizable-strings does not complain about unbalanced single quotes.
+        '''
+
+        self.assertEquals(57354, parse_bug_id_from_changelog(commit_text))
+
+    def test_parse_log_entries_from_changelog(self):
+        changelog_file = StringIO(self._example_changelog)
+        parsed_entries = list(ChangeLog.parse_entries_from_file(changelog_file))
+        self.assertEquals(len(parsed_entries), 9)
+        self.assertEquals(parsed_entries[0].reviewer_text(), "David Levin")
+        self.assertEquals(parsed_entries[1].author_email(), "ddkilzer@apple.com")
+        self.assertEquals(parsed_entries[2].reviewer_text(), "Mark Rowe")
+        self.assertEquals(parsed_entries[2].touched_files(), ["DumpRenderTree/mac/DumpRenderTreeWindow.mm"])
+        self.assertEquals(parsed_entries[3].author_name(), "Benjamin Poulain")
+        self.assertEquals(parsed_entries[3].touched_files(), ["platform/cf/KURLCFNet.cpp", "platform/mac/KURLMac.mm",
+            "WebCoreSupport/ChromeClientEfl.cpp", "ewk/ewk_private.h", "ewk/ewk_view.cpp"])
+        self.assertEquals(parsed_entries[4].reviewer_text(), "David Hyatt")
+        self.assertEquals(parsed_entries[5].reviewer_text(), "Adam Roben")
+        self.assertEquals(parsed_entries[6].reviewer_text(), "Tony Chang")
+        self.assertEquals(parsed_entries[7].reviewer_text(), None)
+        self.assertEquals(parsed_entries[8].reviewer_text(), 'Darin Adler')
+
+    def test_parse_log_entries_from_annotated_file(self):
+        # Note that there are trailing spaces on some of the lines intentionally.
+        changelog_file = StringIO(u"100000 ossy@webkit.org 2011-11-11  Csaba Osztrogon\u00e1c  <ossy@webkit.org>\n"
+            u"100000 ossy@webkit.org\n"
+            u"100000 ossy@webkit.org         100,000 !!!\n"
+            u"100000 ossy@webkit.org \n"
+            u"100000 ossy@webkit.org         Reviewed by Zoltan Herczeg.\n"
+            u"100000 ossy@webkit.org \n"
+            u"100000 ossy@webkit.org         * ChangeLog: Point out revision 100,000.\n"
+            u"100000 ossy@webkit.org \n"
+            u"93798 ap@apple.com 2011-08-25  Alexey Proskuryakov  <ap@apple.com>\n"
+            u"93798 ap@apple.com \n"
+            u"93798 ap@apple.com         Fix build when GCC 4.2 is not installed.\n"
+            u"93798 ap@apple.com \n"
+            u"93798 ap@apple.com         * gtest/xcode/Config/CompilerVersion.xcconfig: Copied from Source/WebCore/Configurations/CompilerVersion.xcconfig.\n"
+            u"93798 ap@apple.com         * gtest/xcode/Config/General.xcconfig:\n"
+            u"93798 ap@apple.com         Use the same compiler version as other projects do.\n"
+            u"93798 ap@apple.com\n"
+            u"99491 andreas.kling@nokia.com 2011-11-03  Andreas Kling  <kling@webkit.org>\n"
+            u"99491 andreas.kling@nokia.com \n"
+            u"99190 andreas.kling@nokia.com         Unreviewed build fix, sigh.\n"
+            u"99190 andreas.kling@nokia.com \n"
+            u"99190 andreas.kling@nokia.com         * css/CSSFontFaceRule.h:\n"
+            u"99190 andreas.kling@nokia.com         * css/CSSMutableStyleDeclaration.h:\n"
+            u"99190 andreas.kling@nokia.com\n"
+            u"99190 andreas.kling@nokia.com 2011-11-03  Andreas Kling  <kling@webkit.org>\n"
+            u"99190 andreas.kling@nokia.com \n"
+            u"99187 andreas.kling@nokia.com         Unreviewed build fix, out-of-line StyleSheet::parentStyleSheet()\n"
+            u"99187 andreas.kling@nokia.com         again since there's a cycle in the includes between CSSRule/StyleSheet.\n"
+            u"99187 andreas.kling@nokia.com \n"
+            u"99187 andreas.kling@nokia.com         * css/StyleSheet.cpp:\n"
+            u"99187 andreas.kling@nokia.com         (WebCore::StyleSheet::parentStyleSheet):\n"
+            u"99187 andreas.kling@nokia.com         * css/StyleSheet.h:\n"
+            u"99187 andreas.kling@nokia.com \n")
+
+        parsed_entries = list(ChangeLog.parse_entries_from_file(changelog_file))
+        self.assertEquals(parsed_entries[0].revision(), 100000)
+        self.assertEquals(parsed_entries[0].reviewer_text(), "Zoltan Herczeg")
+        self.assertEquals(parsed_entries[0].author_name(), u"Csaba Osztrogon\u00e1c")
+        self.assertEquals(parsed_entries[0].author_email(), "ossy@webkit.org")
+        self.assertEquals(parsed_entries[1].revision(), 93798)
+        self.assertEquals(parsed_entries[1].author_name(), "Alexey Proskuryakov")
+        self.assertEquals(parsed_entries[2].revision(), 99190)
+        self.assertEquals(parsed_entries[2].author_name(), "Andreas Kling")
+        self.assertEquals(parsed_entries[3].revision(), 99187)
+        self.assertEquals(parsed_entries[3].author_name(), "Andreas Kling")
+
+    def _assert_parse_reviewer_text_and_list(self, text, expected_reviewer_text, expected_reviewer_text_list=None):
+        reviewer_text, reviewer_text_list = ChangeLogEntry._parse_reviewer_text(text)
+        self.assertEquals(reviewer_text, expected_reviewer_text)
+        if expected_reviewer_text_list:
+            self.assertEquals(reviewer_text_list, expected_reviewer_text_list)
+        else:
+            self.assertEquals(reviewer_text_list, [expected_reviewer_text])
+
+    def _assert_parse_reviewer_text_list(self, text, expected_reviewer_text_list):
+        reviewer_text, reviewer_text_list = ChangeLogEntry._parse_reviewer_text(text)
+        self.assertEquals(reviewer_text_list, expected_reviewer_text_list)
+
+    def test_parse_reviewer_text(self):
+        self._assert_parse_reviewer_text_and_list('  reviewed  by Ryosuke Niwa,   Oliver Hunt, and  Dimitri Glazkov',
+            'Ryosuke Niwa, Oliver Hunt, and Dimitri Glazkov', ['Ryosuke Niwa', 'Oliver Hunt', 'Dimitri Glazkov'])
+        self._assert_parse_reviewer_text_and_list('Reviewed by Brady Eidson and David Levin, landed by Brady Eidson',
+            'Brady Eidson and David Levin', ['Brady Eidson', 'David Levin'])
+
+        self._assert_parse_reviewer_text_and_list('Reviewed by Simon Fraser. Committed by Beth Dakin.', 'Simon Fraser')
+        self._assert_parse_reviewer_text_and_list('Reviewed by Geoff Garen. V8 fixes courtesy of Dmitry Titov.', 'Geoff Garen')
+        self._assert_parse_reviewer_text_and_list('Reviewed by Adam Roben&Dirk Schulze', 'Adam Roben&Dirk Schulze', ['Adam Roben', 'Dirk Schulze'])
+        self._assert_parse_reviewer_text_and_list('Rubber stamps by Darin Adler & Sam Weinig.', 'Darin Adler & Sam Weinig', ['Darin Adler', 'Sam Weinig'])
+
+        self._assert_parse_reviewer_text_and_list('Reviewed by adam,andy and andy adam, andy smith',
+            'adam,andy and andy adam, andy smith', ['adam', 'andy', 'andy adam', 'andy smith'])
+
+        self._assert_parse_reviewer_text_and_list('rubber stamped by Oliver Hunt (oliver@apple.com) and Darin Adler (darin@apple.com)',
+            'Oliver Hunt and Darin Adler', ['Oliver Hunt', 'Darin Adler'])
+
+        self._assert_parse_reviewer_text_and_list('rubber  Stamped by David Hyatt  <hyatt@apple.com>', 'David Hyatt')
+        self._assert_parse_reviewer_text_and_list('Rubber-stamped by Antti Koivisto.', 'Antti Koivisto')
+        self._assert_parse_reviewer_text_and_list('Rubberstamped by Dan Bernstein.', 'Dan Bernstein')
+        self._assert_parse_reviewer_text_and_list('Reviews by Ryosuke Niwa', 'Ryosuke Niwa')
+        self._assert_parse_reviewer_text_and_list('Reviews Ryosuke Niwa', 'Ryosuke Niwa')
+        self._assert_parse_reviewer_text_and_list('Rubberstamp Ryosuke Niwa', 'Ryosuke Niwa')
+        self._assert_parse_reviewer_text_and_list('Typed and reviewed by Alexey Proskuryakov.', 'Alexey Proskuryakov')
+        self._assert_parse_reviewer_text_and_list('Reviewed and landed by Brady Eidson', 'Brady Eidson')
+        self._assert_parse_reviewer_text_and_list('Reviewed by rniwa@webkit.org.', 'rniwa@webkit.org')
+        self._assert_parse_reviewer_text_and_list('Reviewed by Dirk Schulze / Darin Adler.', 'Dirk Schulze / Darin Adler', ['Dirk Schulze', 'Darin Adler'])
+        self._assert_parse_reviewer_text_and_list('Reviewed by Sam Weinig + Oliver Hunt.', 'Sam Weinig + Oliver Hunt', ['Sam Weinig', 'Oliver Hunt'])
+
+        self._assert_parse_reviewer_text_list('Reviewed by Sam Weinig, and given a good once-over by Jeff Miller.', ['Sam Weinig', 'Jeff Miller'])
+        self._assert_parse_reviewer_text_list(' Reviewed by Sam Weinig, even though this is just a...', ['Sam Weinig'])
+        self._assert_parse_reviewer_text_list('Rubber stamped by by Gustavo Noronha Silva', ['Gustavo Noronha Silva'])
+        self._assert_parse_reviewer_text_list('Rubberstamped by Noam Rosenthal, who wrote the original code.', ['Noam Rosenthal'])
+        self._assert_parse_reviewer_text_list('Reviewed by Dan Bernstein (relanding of r47157)', ['Dan Bernstein'])
+        self._assert_parse_reviewer_text_list('Reviewed by Geoffrey "Sean/Shawn/Shaun" Garen', ['Geoffrey Garen'])
+        self._assert_parse_reviewer_text_list('Reviewed by Dave "Messy" Hyatt.', ['Dave Hyatt'])
+        self._assert_parse_reviewer_text_list('Reviewed by Sam \'The Belly\' Weinig', ['Sam Weinig'])
+        self._assert_parse_reviewer_text_list('Rubber-stamped by David "I\'d prefer not" Hyatt.', ['David Hyatt'])
+        self._assert_parse_reviewer_text_list('Reviewed by Mr. Geoffrey Garen.', ['Geoffrey Garen'])
+        self._assert_parse_reviewer_text_list('Reviewed by Darin (ages ago)', ['Darin'])
+        self._assert_parse_reviewer_text_list('Reviewed by Sam Weinig (except for a few comment and header tweaks).', ['Sam Weinig'])
+        self._assert_parse_reviewer_text_list('Reviewed by Sam Weinig (all but the FormDataListItem rename)', ['Sam Weinig'])
+        self._assert_parse_reviewer_text_list('Reviewed by Darin Adler, tweaked and landed by Beth.', ['Darin Adler'])
+        self._assert_parse_reviewer_text_list('Reviewed by Sam Weinig with no hesitation', ['Sam Weinig'])
+        self._assert_parse_reviewer_text_list('Reviewed by Oliver Hunt, okayed by Darin Adler.', ['Oliver Hunt'])
+        self._assert_parse_reviewer_text_list('Reviewed by Darin Adler).', ['Darin Adler'])
+
+        # For now, we let unofficial reviewers recognized as reviewers
+        self._assert_parse_reviewer_text_list('Reviewed by Sam Weinig, Anders Carlsson, and (unofficially) Adam Barth.',
+            ['Sam Weinig', 'Anders Carlsson', 'Adam Barth'])
+
+        self._assert_parse_reviewer_text_list('Reviewed by NOBODY.', None)
+        self._assert_parse_reviewer_text_list('Reviewed by NOBODY - Build Fix.', None)
+        self._assert_parse_reviewer_text_list('Reviewed by NOBODY, layout tests fix.', None)
+        self._assert_parse_reviewer_text_list('Reviewed by NOBODY (Qt build fix pt 2).', None)
+        self._assert_parse_reviewer_text_list('Reviewed by NOBODY(rollout)', None)
+        self._assert_parse_reviewer_text_list('Reviewed by NOBODY (Build fix, forgot to svn add this file)', None)
+        self._assert_parse_reviewer_text_list('Reviewed by nobody (trivial follow up fix), Joseph Pecoraro LGTM-ed.', None)
+
+    def _entry_with_author(self, author_text):
+        return ChangeLogEntry('''2009-08-19  AUTHOR_TEXT
+
+            Reviewed by Ryosuke Niwa
+
+            * Scripts/bugzilla-tool:
+'''.replace("AUTHOR_TEXT", author_text))
+
+    def _entry_with_reviewer(self, reviewer_line):
+        return ChangeLogEntry('''2009-08-19  Eric Seidel  <eric@webkit.org>
+
+            REVIEW_LINE
+
+            * Scripts/bugzilla-tool:
+'''.replace("REVIEW_LINE", reviewer_line))
+
+    def _contributors(self, names):
+        return [CommitterList().contributor_by_name(name) for name in names]
+
+    def _assert_fuzzy_reviewer_match(self, reviewer_text, expected_text_list, expected_contributors):
+        unused, reviewer_text_list = ChangeLogEntry._parse_reviewer_text(reviewer_text)
+        self.assertEquals(reviewer_text_list, expected_text_list)
+        self.assertEquals(self._entry_with_reviewer(reviewer_text).reviewers(), self._contributors(expected_contributors))
+
+    def test_fuzzy_reviewer_match__none(self):
+        self._assert_fuzzy_reviewer_match('Reviewed by BUILD FIX', ['BUILD FIX'], [])
+        self._assert_fuzzy_reviewer_match('Reviewed by Mac build fix', ['Mac build fix'], [])
+
+    def test_fuzzy_reviewer_match_adam_barth(self):
+        self._assert_fuzzy_reviewer_match('Reviewed by Adam Barth.:w', ['Adam Barth.:w'], ['Adam Barth'])
+
+    def test_fuzzy_reviewer_match_darin_adler_et_al(self):
+        self._assert_fuzzy_reviewer_match('Reviewed by Darin Adler in <https://bugs.webkit.org/show_bug.cgi?id=47736>.', ['Darin Adler in'], ['Darin Adler'])
+        self._assert_fuzzy_reviewer_match('Reviewed by Darin Adler, Dan Bernstein, Adele Peterson, and others.',
+            ['Darin Adler', 'Dan Bernstein', 'Adele Peterson', 'others'], ['Darin Adler', 'Dan Bernstein', 'Adele Peterson'])
+
+    def test_fuzzy_reviewer_match_dimitri_glazkov(self):
+        self._assert_fuzzy_reviewer_match('Reviewed by Dimitri Glazkov, build fix', ['Dimitri Glazkov', 'build fix'], ['Dimitri Glazkov'])
+
+    def test_fuzzy_reviewer_match_george_staikos(self):
+        self._assert_fuzzy_reviewer_match('Reviewed by George Staikos (and others)', ['George Staikos', 'others'], ['George Staikos'])
+
+    def test_fuzzy_reviewer_match_mark_rowe(self):
+        self._assert_fuzzy_reviewer_match('Reviewed by Mark Rowe, but Dan Bernstein also reviewed and asked thoughtful questions.',
+            ['Mark Rowe', 'but Dan Bernstein also reviewed', 'asked thoughtful questions'], ['Mark Rowe'])
+
+    def test_fuzzy_reviewer_match_initial(self):
+        self._assert_fuzzy_reviewer_match('Reviewed by Alejandro G. Castro.',
+            ['Alejandro G. Castro'], ['Alejandro G. Castro'])
+        self._assert_fuzzy_reviewer_match('Reviewed by G. Alejandro G. Castro and others.',
+            ['G. Alejandro G. Castro', 'others'], ['Alejandro G. Castro'])
+
+        # If a reviewer has a name that ended with an initial, the regular expression
+        # will incorrectly trim the last period, but it will still match fuzzily to
+        # the full reviewer name.
+        self._assert_fuzzy_reviewer_match('Reviewed by G. Alejandro G. G. Castro G.',
+            ['G. Alejandro G. G. Castro G'], ['Alejandro G. Castro'])
+
+    def _assert_parse_authors(self, author_text, expected_contributors):
+        parsed_authors = [(author['name'], author['email']) for author in self._entry_with_author(author_text).authors()]
+        self.assertEquals(parsed_authors, expected_contributors)
+
+    def test_parse_authors(self):
+        self._assert_parse_authors(u'Aaron Colwell  <acolwell@chromium.org>', [(u'Aaron Colwell', u'acolwell@chromium.org')])
+        self._assert_parse_authors('Eric Seidel  <eric@webkit.org>, Ryosuke Niwa  <rniwa@webkit.org>',
+            [('Eric Seidel', 'eric@webkit.org'), ('Ryosuke Niwa', 'rniwa@webkit.org')])
+        self._assert_parse_authors('Zan Dobersek  <zandobersek@gmail.com> and Philippe Normand  <pnormand@igalia.com>',
+            [('Zan Dobersek', 'zandobersek@gmail.com'), ('Philippe Normand', 'pnormand@igalia.com')])
+        self._assert_parse_authors('New Contributor  <new@webkit.org> and Noob  <noob@webkit.org>',
+            [('New Contributor', 'new@webkit.org'), ('Noob', 'noob@webkit.org')])
+        self._assert_parse_authors('Adam Barth  <abarth@webkit.org> && Benjamin Poulain  <bpoulain@apple.com>',
+            [('Adam Barth', 'abarth@webkit.org'), ('Benjamin Poulain', 'bpoulain@apple.com')])
+
+    def _assert_has_valid_reviewer(self, reviewer_line, expected):
+        self.assertEqual(self._entry_with_reviewer(reviewer_line).has_valid_reviewer(), expected)
+
+    def test_has_valid_reviewer(self):
+        self._assert_has_valid_reviewer("Reviewed by Eric Seidel.", True)
+        self._assert_has_valid_reviewer("Reviewed by Eric Seidel", True)  # Not picky about the '.'
+        self._assert_has_valid_reviewer("Reviewed by Eric.", False)
+        self._assert_has_valid_reviewer("Reviewed by Eric C Seidel.", False)
+        self._assert_has_valid_reviewer("Rubber-stamped by Eric.", False)
+        self._assert_has_valid_reviewer("Rubber-stamped by Eric Seidel.", True)
+        self._assert_has_valid_reviewer("Rubber stamped by Eric.", False)
+        self._assert_has_valid_reviewer("Rubber stamped by Eric Seidel.", True)
+        self._assert_has_valid_reviewer("Unreviewed build fix.", True)
+
+    def test_latest_entry_parse(self):
+        changelog_contents = u"%s\n%s" % (self._example_entry, self._example_changelog)
+        changelog_file = StringIO(changelog_contents)
+        latest_entry = ChangeLog.parse_latest_entry_from_file(changelog_file)
+        self.assertEquals(latest_entry.contents(), self._example_entry)
+        self.assertEquals(latest_entry.author_name(), "Peter Kasting")
+        self.assertEquals(latest_entry.author_email(), "pkasting@google.com")
+        self.assertEquals(latest_entry.reviewer_text(), u"Tor Arne Vestb\xf8")
+        self.assertEquals(latest_entry.touched_files(), ["DumpRenderTree/win/DumpRenderTree.vcproj", "DumpRenderTree/win/ImageDiff.vcproj", "DumpRenderTree/win/TestNetscapePlugin/TestNetscapePlugin.vcproj"])
+
+        self.assertTrue(latest_entry.reviewer())  # Make sure that our UTF8-based lookup of Tor works.
+
+    def test_latest_entry_parse_single_entry(self):
+        changelog_contents = u"%s\n%s" % (self._example_entry, self._rolled_over_footer)
+        changelog_file = StringIO(changelog_contents)
+        latest_entry = ChangeLog.parse_latest_entry_from_file(changelog_file)
+        self.assertEquals(latest_entry.contents(), self._example_entry)
+        self.assertEquals(latest_entry.author_name(), "Peter Kasting")
+
+    @staticmethod
+    def _write_tmp_file_with_contents(byte_array):
+        assert(isinstance(byte_array, str))
+        (file_descriptor, file_path) = tempfile.mkstemp() # NamedTemporaryFile always deletes the file on close in python < 2.6
+        with os.fdopen(file_descriptor, "w") as file:
+            file.write(byte_array)
+        return file_path
+
+    @staticmethod
+    def _read_file_contents(file_path, encoding):
+        with codecs.open(file_path, "r", encoding) as file:
+            return file.read()
+
+    # FIXME: We really should be getting this from prepare-ChangeLog itself.
+    _new_entry_boilerplate = '''2009-08-19  Eric Seidel  <eric@webkit.org>
+
+        Need a short description (OOPS!).
+        Need the bug URL (OOPS!).
+
+        Reviewed by NOBODY (OOPS!).
+
+        * Scripts/bugzilla-tool:
+'''
+
+    _new_entry_boilerplate_with_bugurl = '''2009-08-19  Eric Seidel  <eric@webkit.org>
+
+        Need a short description (OOPS!).
+        https://bugs.webkit.org/show_bug.cgi?id=12345
+
+        Reviewed by NOBODY (OOPS!).
+
+        * Scripts/bugzilla-tool:
+'''
+
+    _new_entry_boilerplate_with_multiple_bugurl = '''2009-08-19  Eric Seidel  <eric@webkit.org>
+
+        Need a short description (OOPS!).
+        https://bugs.webkit.org/show_bug.cgi?id=12345
+        http://webkit.org/b/12345
+
+        Reviewed by NOBODY (OOPS!).
+
+        * Scripts/bugzilla-tool:
+'''
+
+    _new_entry_boilerplate_without_reviewer_line = '''2009-08-19  Eric Seidel  <eric@webkit.org>
+
+        Need a short description (OOPS!).
+        https://bugs.webkit.org/show_bug.cgi?id=12345
+
+        * Scripts/bugzilla-tool:
+'''
+
+    _new_entry_boilerplate_without_reviewer_multiple_bugurl = '''2009-08-19  Eric Seidel  <eric@webkit.org>
+
+        Need a short description (OOPS!).
+        https://bugs.webkit.org/show_bug.cgi?id=12345
+        http://webkit.org/b/12345
+
+        * Scripts/bugzilla-tool:
+'''
+
+    def test_set_reviewer(self):
+        changelog_contents = u"%s\n%s" % (self._new_entry_boilerplate_with_bugurl, self._example_changelog)
+        changelog_path = self._write_tmp_file_with_contents(changelog_contents.encode("utf-8"))
+        reviewer_name = 'Test Reviewer'
+        ChangeLog(changelog_path).set_reviewer(reviewer_name)
+        actual_contents = self._read_file_contents(changelog_path, "utf-8")
+        expected_contents = changelog_contents.replace('NOBODY (OOPS!)', reviewer_name)
+        os.remove(changelog_path)
+        self.assertEquals(actual_contents.splitlines(), expected_contents.splitlines())
+
+        changelog_contents_without_reviewer_line = u"%s\n%s" % (self._new_entry_boilerplate_without_reviewer_line, self._example_changelog)
+        changelog_path = self._write_tmp_file_with_contents(changelog_contents_without_reviewer_line.encode("utf-8"))
+        ChangeLog(changelog_path).set_reviewer(reviewer_name)
+        actual_contents = self._read_file_contents(changelog_path, "utf-8")
+        os.remove(changelog_path)
+        self.assertEquals(actual_contents.splitlines(), expected_contents.splitlines())
+
+        changelog_contents_without_reviewer_line = u"%s\n%s" % (self._new_entry_boilerplate_without_reviewer_multiple_bugurl, self._example_changelog)
+        changelog_path = self._write_tmp_file_with_contents(changelog_contents_without_reviewer_line.encode("utf-8"))
+        ChangeLog(changelog_path).set_reviewer(reviewer_name)
+        actual_contents = self._read_file_contents(changelog_path, "utf-8")
+        changelog_contents = u"%s\n%s" % (self._new_entry_boilerplate_with_multiple_bugurl, self._example_changelog)
+        expected_contents = changelog_contents.replace('NOBODY (OOPS!)', reviewer_name)
+        os.remove(changelog_path)
+        self.assertEquals(actual_contents.splitlines(), expected_contents.splitlines())
+
+    def test_set_short_description_and_bug_url(self):
+        changelog_contents = u"%s\n%s" % (self._new_entry_boilerplate_with_bugurl, self._example_changelog)
+        changelog_path = self._write_tmp_file_with_contents(changelog_contents.encode("utf-8"))
+        short_description = "A short description"
+        bug_url = "http://example.com/b/2344"
+        ChangeLog(changelog_path).set_short_description_and_bug_url(short_description, bug_url)
+        actual_contents = self._read_file_contents(changelog_path, "utf-8")
+        expected_message = "%s\n        %s" % (short_description, bug_url)
+        expected_contents = changelog_contents.replace("Need a short description (OOPS!).", expected_message)
+        os.remove(changelog_path)
+        self.assertEquals(actual_contents.splitlines(), expected_contents.splitlines())
+
+        changelog_contents = u"%s\n%s" % (self._new_entry_boilerplate, self._example_changelog)
+        changelog_path = self._write_tmp_file_with_contents(changelog_contents.encode("utf-8"))
+        short_description = "A short description 2"
+        bug_url = "http://example.com/b/2345"
+        ChangeLog(changelog_path).set_short_description_and_bug_url(short_description, bug_url)
+        actual_contents = self._read_file_contents(changelog_path, "utf-8")
+        expected_message = "%s\n        %s" % (short_description, bug_url)
+        expected_contents = changelog_contents.replace("Need a short description (OOPS!).\n        Need the bug URL (OOPS!).", expected_message)
+        os.remove(changelog_path)
+        self.assertEquals(actual_contents.splitlines(), expected_contents.splitlines())
diff --git a/Tools/Scripts/webkitpy/common/checkout/checkout.py b/Tools/Scripts/webkitpy/common/checkout/checkout.py
new file mode 100644
index 0000000..8f45024
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/checkout.py
@@ -0,0 +1,178 @@
+# 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 StringIO
+
+from webkitpy.common.config import urls
+from webkitpy.common.checkout.changelog import ChangeLog, parse_bug_id_from_changelog
+from webkitpy.common.checkout.commitinfo import CommitInfo
+from webkitpy.common.checkout.scm import CommitMessage
+from webkitpy.common.checkout.deps import DEPS
+from webkitpy.common.memoized import memoized
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.system.deprecated_logging import log
+
+
+# This class represents the WebKit-specific parts of the checkout (like ChangeLogs).
+# FIXME: Move a bunch of ChangeLog-specific processing from SCM to this object.
+# NOTE: All paths returned from this class should be absolute.
+class Checkout(object):
+    def __init__(self, scm, executive=None, filesystem=None):
+        self._scm = scm
+        # FIXME: We shouldn't be grabbing at private members on scm.
+        self._executive = executive or self._scm._executive
+        self._filesystem = filesystem or self._scm._filesystem
+
+    def is_path_to_changelog(self, path):
+        return self._filesystem.basename(path) == "ChangeLog"
+
+    def _latest_entry_for_changelog_at_revision(self, changelog_path, revision):
+        changelog_contents = self._scm.contents_at_revision(changelog_path, revision)
+        # contents_at_revision returns a byte array (str()), but we know
+        # that ChangeLog files are utf-8.  parse_latest_entry_from_file
+        # expects a file-like object which vends unicode(), so we decode here.
+        # Old revisions of Sources/WebKit/wx/ChangeLog have some invalid utf8 characters.
+        changelog_file = StringIO.StringIO(changelog_contents.decode("utf-8", "ignore"))
+        return ChangeLog.parse_latest_entry_from_file(changelog_file)
+
+    def changelog_entries_for_revision(self, revision, changed_files=None):
+        if not changed_files:
+            changed_files = self._scm.changed_files_for_revision(revision)
+        # FIXME: This gets confused if ChangeLog files are moved, as
+        # deletes are still "changed files" per changed_files_for_revision.
+        # FIXME: For now we hack around this by caching any exceptions
+        # which result from having deleted files included the changed_files list.
+        changelog_entries = []
+        for path in changed_files:
+            if not self.is_path_to_changelog(path):
+                continue
+            try:
+                changelog_entries.append(self._latest_entry_for_changelog_at_revision(path, revision))
+            except ScriptError:
+                pass
+        return changelog_entries
+
+    def _changelog_data_for_revision(self, revision):
+        changed_files = self._scm.changed_files_for_revision(revision)
+        changelog_entries = self.changelog_entries_for_revision(revision, changed_files=changed_files)
+        # Assume for now that the first entry has everything we need:
+        # FIXME: This will throw an exception if there were no ChangeLogs.
+        if not len(changelog_entries):
+            return None
+        changelog_entry = changelog_entries[0]
+        return {
+            "bug_id": parse_bug_id_from_changelog(changelog_entry.contents()),
+            "author_name": changelog_entry.author_name(),
+            "author_email": changelog_entry.author_email(),
+            "author": changelog_entry.author(),
+            "reviewer_text": changelog_entry.reviewer_text(),
+            "reviewer": changelog_entry.reviewer(),
+            "contents": changelog_entry.contents(),
+            "changed_files": changed_files,
+        }
+
+    @memoized
+    def commit_info_for_revision(self, revision):
+        committer_email = self._scm.committer_email_for_revision(revision)
+        changelog_data = self._changelog_data_for_revision(revision)
+        if not changelog_data:
+            return None
+        return CommitInfo(revision, committer_email, changelog_data)
+
+    def bug_id_for_revision(self, revision):
+        return self.commit_info_for_revision(revision).bug_id()
+
+    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.
+        if not changed_files:
+            changed_files = self._scm.changed_files(git_commit)
+        return filter(predicate, map(self._scm.absolute_path, changed_files))
+
+    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, changed_files=None, return_stderr=False):
+        changelog_paths = self.modified_changelogs(git_commit, changed_files)
+        if not len(changelog_paths):
+            raise ScriptError(message="Found no modified ChangeLogs, cannot create a commit message.\n"
+                              "All changes require a ChangeLog.  See:\n %s" % urls.contribution_guidelines)
+
+        message_text = self._scm.run([self._scm.script_path('commit-log-editor'), '--print-log'] + changelog_paths, return_stderr=return_stderr)
+        return CommitMessage(message_text.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=None):
+        changed_files = self.modified_non_changelogs(git_commit, changed_files)
+        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, changed_files=None):
+        try:
+            return parse_bug_id_from_changelog(self.commit_message_for_this_commit(git_commit, changed_files).message())
+        except ScriptError, e:
+            pass # We might not have ChangeLogs.
+
+    def chromium_deps(self):
+        return DEPS(self._scm.absolute_path(self._filesystem.join("Source", "WebKit", "chromium", "DEPS")))
+
+    def apply_patch(self, patch):
+        # It's possible that the patch was not made from the root directory.
+        # We should detect and handle that case.
+        # FIXME: Move _scm.script_path here once we get rid of all the dependencies.
+        # --force (continue after errors) is the common case, so we always use it.
+        args = [self._scm.script_path('svn-apply'), "--force"]
+        if patch.reviewer():
+            args += ['--reviewer', patch.reviewer().full_name]
+        self._executive.run_command(args, input=patch.contents(), cwd=self._scm.checkout_root)
+
+    def apply_reverse_diff(self, revision):
+        self._scm.apply_reverse_diff(revision)
+
+        # We revert the ChangeLogs because removing lines from a ChangeLog
+        # doesn't make sense.  ChangeLogs are append only.
+        changelog_paths = self.modified_changelogs(git_commit=None)
+        if len(changelog_paths):
+            self._scm.revert_files(changelog_paths)
+
+        conflicts = self._scm.conflicted_files()
+        if len(conflicts):
+            raise ScriptError(message="Failed to apply reverse diff for revision %s because of the following conflicts:\n%s" % (revision, "\n".join(conflicts)))
+
+    def apply_reverse_diffs(self, revision_list):
+        for revision in sorted(revision_list, reverse=True):
+            self.apply_reverse_diff(revision)
diff --git a/Tools/Scripts/webkitpy/common/checkout/checkout_mock.py b/Tools/Scripts/webkitpy/common/checkout/checkout_mock.py
new file mode 100644
index 0000000..3c050ae
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/checkout_mock.py
@@ -0,0 +1,117 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from .deps_mock import MockDEPS
+from .commitinfo import CommitInfo
+
+# FIXME: These imports are wrong, we should use a shared MockCommittersList.
+from webkitpy.common.config.committers import CommitterList
+from webkitpy.common.net.bugzilla.bugzilla_mock import _mock_reviewers
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+
+
+class MockCommitMessage(object):
+    def message(self):
+        return "This is a fake commit message that is at least 50 characters."
+
+
+committer_list = CommitterList()
+
+mock_revisions = {
+    1: CommitInfo(852, "eric@webkit.org", {
+        "bug_id": 50000,
+        "author_name": "Adam Barth",
+        "author_email": "abarth@webkit.org",
+        "author": committer_list.contributor_by_email("abarth@webkit.org"),
+        "reviewer_text": "Darin Adler",
+        "reviewer": committer_list.committer_by_name("Darin Adler"),
+        "changed_files": [
+            "path/to/file",
+            "another/file",
+        ],
+    }),
+    3001: CommitInfo(3001, "tomz@codeaurora.org", {
+        "bug_id": 50004,
+        "author_name": "Tom Zakrajsek",
+        "author_email": "tomz@codeaurora.org",
+        "author": committer_list.contributor_by_email("tomz@codeaurora.org"),
+        "reviewer_text": "Darin Adler",
+        "reviewer": committer_list.committer_by_name("Darin Adler"),
+        "changed_files": [
+            "path/to/file",
+            "another/file",
+        ],
+    })
+}
+
+class MockCheckout(object):
+    def __init__(self):
+        # FIXME: It's unclear if a MockCheckout is very useful.  A normal Checkout
+        # with a MockSCM/MockFileSystem/MockExecutive is probably better.
+        self._filesystem = MockFileSystem()
+
+    def commit_info_for_revision(self, svn_revision):
+        # There are legacy tests that all expected these revision numbers to map
+        # to the same commit description (now mock_revisions[1])
+        if svn_revision in [32, 123, 852, 853, 854, 1234, 21654, 21655, 21656]:
+            return mock_revisions[1]
+
+        if svn_revision in mock_revisions:
+            return mock_revisions[svn_revision]
+
+        # any "unrecognized" svn_revision will return None.
+
+    def is_path_to_changelog(self, path):
+        return self._filesystem.basename(path) == "ChangeLog"
+
+    def bug_id_for_revision(self, svn_revision):
+        return 12345
+
+    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 []
+
+    def commit_message_for_this_commit(self, git_commit, changed_files=None):
+        return MockCommitMessage()
+
+    def chromium_deps(self):
+        return MockDEPS()
+
+    def apply_patch(self, patch):
+        pass
+
+    def apply_reverse_diffs(self, revision):
+        pass
+
+    def suggested_reviewers(self, git_commit, changed_files=None):
+        # FIXME: We should use a shared mock commiter list.
+        return [_mock_reviewers[0]]
diff --git a/Tools/Scripts/webkitpy/common/checkout/checkout_unittest.py b/Tools/Scripts/webkitpy/common/checkout/checkout_unittest.py
new file mode 100644
index 0000000..e9c2cdd
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/checkout_unittest.py
@@ -0,0 +1,263 @@
+# 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 codecs
+import os
+import shutil
+import tempfile
+import unittest
+
+from .checkout import Checkout
+from .changelog import ChangeLogEntry
+from .scm import CommitMessage, SCMDetector
+from .scm.scm_mock import MockSCM
+from webkitpy.common.system.executive import Executive, ScriptError
+from webkitpy.common.system.filesystem import FileSystem  # FIXME: This should not be needed.
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.executive_mock import MockExecutive
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.thirdparty.mock import Mock
+
+
+_changelog1entry1 = u"""2010-03-25  Tor Arne Vestb\u00f8  <vestbo@webkit.org>
+
+        Unreviewed build fix to un-break webkit-patch land.
+
+        Move commit_message_for_this_commit from scm to checkout
+        https://bugs.webkit.org/show_bug.cgi?id=36629
+
+        * Scripts/webkitpy/common/checkout/api.py: import scm.CommitMessage
+"""
+_changelog1entry2 = u"""2010-03-25  Adam Barth  <abarth@webkit.org>
+
+        Reviewed by Eric Seidel.
+
+        Move commit_message_for_this_commit from scm to checkout
+        https://bugs.webkit.org/show_bug.cgi?id=36629
+
+        * Scripts/webkitpy/common/checkout/api.py:
+"""
+_changelog1 = u"\n".join([_changelog1entry1, _changelog1entry2])
+_changelog2 = u"""2010-03-25  Tor Arne Vestb\u00f8  <vestbo@webkit.org>
+
+        Unreviewed build fix to un-break webkit-patch land.
+
+        Second part of this complicated change by me, Tor Arne Vestb\u00f8!
+
+        * Path/To/Complicated/File: Added.
+
+2010-03-25  Adam Barth  <abarth@webkit.org>
+
+        Reviewed by Eric Seidel.
+
+        Filler change.
+"""
+
+class CommitMessageForThisCommitTest(unittest.TestCase):
+    expected_commit_message = u"""Unreviewed build fix to un-break webkit-patch land.
+
+Tools: 
+
+Move commit_message_for_this_commit from scm to checkout
+https://bugs.webkit.org/show_bug.cgi?id=36629
+
+* Scripts/webkitpy/common/checkout/api.py: import scm.CommitMessage
+
+LayoutTests: 
+
+Second part of this complicated change by me, Tor Arne Vestb\u00f8!
+
+* Path/To/Complicated/File: Added.
+"""
+
+    def setUp(self):
+        # FIXME: This should not need to touch the filesystem, however
+        # ChangeLog is difficult to mock at current.
+        self.filesystem = FileSystem()
+        self.temp_dir = str(self.filesystem.mkdtemp(suffix="changelogs"))
+        self.old_cwd = self.filesystem.getcwd()
+        self.filesystem.chdir(self.temp_dir)
+
+        # Trick commit-log-editor into thinking we're in a Subversion working copy so it won't
+        # complain about not being able to figure out what SCM is in use.
+        # FIXME: VCSTools.pm is no longer so easily fooled.  It logs because "svn info" doesn't
+        # treat a bare .svn directory being part of an svn checkout.
+        self.filesystem.maybe_make_directory(".svn")
+
+        self.changelogs = map(self.filesystem.abspath, (self.filesystem.join("Tools", "ChangeLog"), self.filesystem.join("LayoutTests", "ChangeLog")))
+        for path, contents in zip(self.changelogs, (_changelog1, _changelog2)):
+            self.filesystem.maybe_make_directory(self.filesystem.dirname(path))
+            self.filesystem.write_text_file(path, contents)
+
+    def tearDown(self):
+        self.filesystem.rmtree(self.temp_dir)
+        self.filesystem.chdir(self.old_cwd)
+
+    def test_commit_message_for_this_commit(self):
+        executive = Executive()
+
+        def mock_run(*args, **kwargs):
+            # Note that we use a real Executive here, not a MockExecutive, so we can test that we're
+            # invoking commit-log-editor correctly.
+            env = os.environ.copy()
+            env['CHANGE_LOG_EMAIL_ADDRESS'] = 'vestbo@webkit.org'
+            kwargs['env'] = env
+            return executive.run_command(*args, **kwargs)
+
+        detector = SCMDetector(self.filesystem, executive)
+        real_scm = detector.detect_scm_system(self.old_cwd)
+
+        mock_scm = MockSCM()
+        mock_scm.run = mock_run
+        mock_scm.script_path = real_scm.script_path
+
+        checkout = Checkout(mock_scm)
+        checkout.modified_changelogs = lambda git_commit, changed_files=None: self.changelogs
+        commit_message = checkout.commit_message_for_this_commit(git_commit=None, return_stderr=True)
+        # Throw away the first line - a warning about unknown VCS root.
+        commit_message.message_lines = commit_message.message_lines[1:]
+        self.assertEqual(commit_message.message(), self.expected_commit_message)
+
+
+class CheckoutTest(unittest.TestCase):
+    def _make_checkout(self):
+        return Checkout(scm=MockSCM(), filesystem=MockFileSystem(), executive=MockExecutive())
+
+    def test_latest_entry_for_changelog_at_revision(self):
+        def mock_contents_at_revision(changelog_path, revision):
+            self.assertEqual(changelog_path, "foo")
+            self.assertEqual(revision, "bar")
+            # contents_at_revision is expected to return a byte array (str)
+            # so we encode our unicode ChangeLog down to a utf-8 stream.
+            # The ChangeLog utf-8 decoding should ignore invalid codepoints.
+            invalid_utf8 = "\255"
+            return _changelog1.encode("utf-8") + invalid_utf8
+        checkout = self._make_checkout()
+        checkout._scm.contents_at_revision = mock_contents_at_revision
+        entry = checkout._latest_entry_for_changelog_at_revision("foo", "bar")
+        self.assertEqual(entry.contents(), _changelog1entry1)
+
+    # FIXME: This tests a hack around our current changed_files handling.
+    # Right now changelog_entries_for_revision tries to fetch deleted files
+    # from revisions, resulting in a ScriptError exception.  Test that we
+    # recover from those and still return the other ChangeLog entries.
+    def test_changelog_entries_for_revision(self):
+        checkout = self._make_checkout()
+        checkout._scm.changed_files_for_revision = lambda revision: ['foo/ChangeLog', 'bar/ChangeLog']
+
+        def mock_latest_entry_for_changelog_at_revision(path, revision):
+            if path == "foo/ChangeLog":
+                return 'foo'
+            raise ScriptError()
+
+        checkout._latest_entry_for_changelog_at_revision = mock_latest_entry_for_changelog_at_revision
+
+        # Even though fetching one of the entries failed, the other should succeed.
+        entries = checkout.changelog_entries_for_revision(1)
+        self.assertEqual(len(entries), 1)
+        self.assertEqual(entries[0], 'foo')
+
+    def test_commit_info_for_revision(self):
+        checkout = self._make_checkout()
+        checkout._scm.changed_files_for_revision = lambda revision: ['path/to/file', 'another/file']
+        checkout._scm.committer_email_for_revision = lambda revision, changed_files=None: "committer@example.com"
+        checkout.changelog_entries_for_revision = lambda revision, changed_files=None: [ChangeLogEntry(_changelog1entry1)]
+        commitinfo = checkout.commit_info_for_revision(4)
+        self.assertEqual(commitinfo.bug_id(), 36629)
+        self.assertEqual(commitinfo.author_name(), u"Tor Arne Vestb\u00f8")
+        self.assertEqual(commitinfo.author_email(), "vestbo@webkit.org")
+        self.assertEqual(commitinfo.reviewer_text(), None)
+        self.assertEqual(commitinfo.reviewer(), None)
+        self.assertEqual(commitinfo.committer_email(), "committer@example.com")
+        self.assertEqual(commitinfo.committer(), None)
+        self.assertEqual(commitinfo.to_json(), {
+            'bug_id': 36629,
+            'author_email': 'vestbo@webkit.org',
+            'changed_files': [
+                'path/to/file',
+                'another/file',
+            ],
+            'reviewer_text': None,
+            'author_name': u'Tor Arne Vestb\xf8',
+        })
+
+        checkout.changelog_entries_for_revision = lambda revision, changed_files=None: []
+        self.assertEqual(checkout.commit_info_for_revision(1), None)
+
+    def test_bug_id_for_revision(self):
+        checkout = self._make_checkout()
+        checkout._scm.committer_email_for_revision = lambda revision: "committer@example.com"
+        checkout.changelog_entries_for_revision = lambda revision, changed_files=None: [ChangeLogEntry(_changelog1entry1)]
+        self.assertEqual(checkout.bug_id_for_revision(4), 36629)
+
+    def test_bug_id_for_this_commit(self):
+        checkout = self._make_checkout()
+        checkout.commit_message_for_this_commit = lambda git_commit, changed_files=None: CommitMessage(ChangeLogEntry(_changelog1entry1).contents().splitlines())
+        self.assertEqual(checkout.bug_id_for_this_commit(git_commit=None), 36629)
+
+    def test_modified_changelogs(self):
+        checkout = self._make_checkout()
+        checkout._scm.checkout_root = "/foo/bar"
+        checkout._scm.changed_files = lambda git_commit: ["file1", "ChangeLog", "relative/path/ChangeLog"]
+        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, changed_files=None):
+            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]
+
+        checkout = self._make_checkout()
+        checkout._scm.checkout_root = "/foo/bar"
+        checkout._scm.changed_files = lambda git_commit: ["file1", "file2", "relative/path/ChangeLog"]
+        checkout._scm.revisions_changing_file = mock_revisions_changing_file
+        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'])
+
+    def test_chromium_deps(self):
+        checkout = self._make_checkout()
+        checkout._scm.checkout_root = "/foo/bar"
+        self.assertEqual(checkout.chromium_deps()._path, '/foo/bar/Source/WebKit/chromium/DEPS')
+
+    def test_apply_patch(self):
+        checkout = self._make_checkout()
+        checkout._executive = MockExecutive(should_log=True)
+        checkout._scm.script_path = lambda script: script
+        mock_patch = Mock()
+        mock_patch.contents = lambda: "foo"
+        mock_patch.reviewer = lambda: None
+        expected_stderr = "MOCK run_command: ['svn-apply', '--force'], cwd=/mock-checkout, input=foo\n"
+        OutputCapture().assert_outputs(self, checkout.apply_patch, [mock_patch], expected_stderr=expected_stderr)
diff --git a/Tools/Scripts/webkitpy/common/checkout/commitinfo.py b/Tools/Scripts/webkitpy/common/checkout/commitinfo.py
new file mode 100644
index 0000000..cba3fdd
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/commitinfo.py
@@ -0,0 +1,100 @@
+# 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.
+#
+# WebKit's python module for holding information on a commit
+
+from webkitpy.common.config import urls
+from webkitpy.common.config.committers import CommitterList
+
+
+class CommitInfo(object):
+    def __init__(self, revision, committer_email, changelog_data, committer_list=CommitterList()):
+        self._revision = revision
+        self._committer_email = committer_email
+        self._changelog_data = changelog_data
+
+        # Derived values:
+        self._committer = committer_list.committer_by_email(committer_email)
+
+    def revision(self):
+        return self._revision
+
+    def committer(self):
+        return self._committer  # None if committer isn't in committers.py
+
+    def committer_email(self):
+        return self._committer_email
+
+    def bug_id(self):
+        return self._changelog_data["bug_id"]  # May be None
+
+    def author(self):
+        return self._changelog_data["author"]  # May be None
+
+    def author_name(self):
+        return self._changelog_data["author_name"]
+
+    def author_email(self):
+        return self._changelog_data["author_email"]
+
+    def reviewer(self):
+        return self._changelog_data["reviewer"]  # May be None
+
+    def reviewer_text(self):
+        return self._changelog_data["reviewer_text"]  # May be None
+
+    def changed_files(self):
+        return self._changelog_data["changed_files"]
+
+    def to_json(self):
+        return {
+            "bug_id": self.bug_id(),
+            "author_name": self.author_name(),
+            "author_email": self.author_email(),
+            "reviewer_text": self.reviewer_text(),
+            "changed_files": self.changed_files(),
+        }
+
+    def responsible_parties(self):
+        responsible_parties = [
+            self.committer(),
+            self.author(),
+            self.reviewer(),
+        ]
+        return set([party for party in responsible_parties if party]) # Filter out None
+
+    # FIXME: It is slightly lame that this "view" method is on this "model" class (in MVC terms)
+    def blame_string(self, bugs):
+        string = "r%s:\n" % self.revision()
+        string += "  %s\n" % urls.view_revision_url(self.revision())
+        string += "  Bug: %s (%s)\n" % (self.bug_id(), bugs.bug_url_for_bug_id(self.bug_id()))
+        author_line = "\"%s\" <%s>" % (self.author_name(), self.author_email())
+        string += "  Author: %s\n" % (self.author() or author_line)
+        string += "  Reviewer: %s\n" % (self.reviewer() or self.reviewer_text())
+        string += "  Committer: %s" % self.committer()
+        return string
diff --git a/Tools/Scripts/webkitpy/common/checkout/commitinfo_unittest.py b/Tools/Scripts/webkitpy/common/checkout/commitinfo_unittest.py
new file mode 100644
index 0000000..f58e6f1
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/commitinfo_unittest.py
@@ -0,0 +1,61 @@
+# 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.checkout.commitinfo import CommitInfo
+from webkitpy.common.config.committers import CommitterList, Committer, Reviewer
+
+class CommitInfoTest(unittest.TestCase):
+
+    def test_commit_info_creation(self):
+        author = Committer("Author", "author@example.com")
+        committer = Committer("Committer", "committer@example.com")
+        reviewer = Reviewer("Reviewer", "reviewer@example.com")
+        committer_list = CommitterList(committers=[author, committer], reviewers=[reviewer])
+
+        changelog_data = {
+            "bug_id": 1234,
+            "author_name": "Committer",
+            "author_email": "author@example.com",
+            "author": author,
+            "reviewer_text": "Reviewer",
+            "reviewer": reviewer,
+        }
+        commit = CommitInfo(123, "committer@example.com", changelog_data, committer_list)
+
+        self.assertEqual(commit.revision(), 123)
+        self.assertEqual(commit.bug_id(), 1234)
+        self.assertEqual(commit.author_name(), "Committer")
+        self.assertEqual(commit.author_email(), "author@example.com")
+        self.assertEqual(commit.author(), author)
+        self.assertEqual(commit.reviewer_text(), "Reviewer")
+        self.assertEqual(commit.reviewer(), reviewer)
+        self.assertEqual(commit.committer(), committer)
+        self.assertEqual(commit.committer_email(), "committer@example.com")
+        self.assertEqual(commit.responsible_parties(), set([author, committer, reviewer]))
diff --git a/Tools/Scripts/webkitpy/common/checkout/deps.py b/Tools/Scripts/webkitpy/common/checkout/deps.py
new file mode 100644
index 0000000..2f3a873
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/deps.py
@@ -0,0 +1,61 @@
+# Copyright (C) 2011, 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.
+#
+# WebKit's Python module for parsing and modifying ChangeLog files
+
+import codecs
+import fileinput
+import re
+import textwrap
+
+
+class DEPS(object):
+
+    _variable_regexp = r"\s+'%s':\s+'(?P<value>\d+)'"
+
+    def __init__(self, path):
+        # FIXME: This should take a FileSystem object.
+        self._path = path
+
+    def read_variable(self, name):
+        pattern = re.compile(self._variable_regexp % name)
+        for line in fileinput.FileInput(self._path):
+            match = pattern.match(line)
+            if match:
+                return int(match.group("value"))
+
+    def write_variable(self, name, value):
+        pattern = re.compile(self._variable_regexp % name)
+        replacement_line = "  '%s': '%s'" % (name, value)
+        # inplace=1 creates a backup file and re-directs stdout to the file
+        for line in fileinput.FileInput(self._path, inplace=1):
+            if pattern.match(line):
+                print replacement_line
+                continue
+            # Trailing comma suppresses printing newline
+            print line,
diff --git a/Tools/Scripts/webkitpy/common/checkout/deps_mock.py b/Tools/Scripts/webkitpy/common/checkout/deps_mock.py
new file mode 100644
index 0000000..cb57e8b
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/deps_mock.py
@@ -0,0 +1,38 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+from webkitpy.common.system.deprecated_logging import log
+
+
+class MockDEPS(object):
+    def read_variable(self, name):
+        return 6564
+
+    def write_variable(self, name, value):
+        log("MOCK: MockDEPS.write_variable(%s, %s)" % (name, value))
diff --git a/Tools/Scripts/webkitpy/common/checkout/diff_parser.py b/Tools/Scripts/webkitpy/common/checkout/diff_parser.py
new file mode 100644
index 0000000..2ed552c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/diff_parser.py
@@ -0,0 +1,186 @@
+# Copyright (C) 2009 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.
+
+"""WebKit's Python module for interacting with patches."""
+
+import logging
+import re
+
+_log = logging.getLogger(__name__)
+
+
+# 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:
+        _regexp_compile_cache[pattern] = re.compile(pattern)
+    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"),
+                           ("^--- \w/(?P<FilePath>.+)", lambda matched: "--- " + matched.group('FilePath') + "\n"),
+                           ("^\+\+\+ \w/(?P<FilePath>.+)", lambda matched: "+++ " + matched.group('FilePath') + "\n"))
+
+    for pattern, conversion in conversion_patterns:
+        matched = match(pattern, line)
+        if matched:
+            return conversion(matched)
+    return line
+
+
+# FIXME: This method belongs on DiffParser
+def get_diff_converter(first_diff_line):
+    """Gets a converter function of diff lines.
+
+    Args:
+      first_diff_line: The first filename line of a diff file.
+                       If this line is git formatted, we'll return a
+                       converter from git to SVN.
+    """
+    if match(r"^diff --git \w/", first_diff_line):
+        return git_diff_to_svn_diff
+    return lambda input: input
+
+
+_INITIAL_STATE = 1
+_DECLARED_FILE_PATH = 2
+_PROCESSING_CHUNK = 3
+
+
+class DiffFile(object):
+    """Contains the information for one file in a patch.
+
+    The field "lines" is a list which contains tuples in this format:
+       (deleted_line_number, new_line_number, line_string)
+    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
+        self.lines = []
+
+    def add_new_line(self, line_number, line):
+        self.lines.append((0, line_number, line))
+
+    def add_deleted_line(self, line_number, line):
+        self.lines.append((line_number, 0, line))
+
+    def add_unchanged_line(self, deleted_line_number, new_line_number, line):
+        self.lines.append((deleted_line_number, new_line_number, line))
+
+
+# If this is going to be called DiffParser, it should be a re-useable parser.
+# Otherwise we should rename it to ParsedDiff or just Diff.
+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.
+    """
+
+    def __init__(self, diff_input):
+        """Parses a diff.
+
+        Args:
+          diff_input: An iterable object.
+        """
+        self.files = self._parse_into_diff_files(diff_input)
+
+    # FIXME: This function is way too long and needs to be broken up.
+    def _parse_into_diff_files(self, diff_input):
+        files = {}
+        state = _INITIAL_STATE
+        current_file = None
+        old_diff_line = None
+        new_diff_line = None
+        for line in diff_input:
+            line = line.rstrip("\n")
+            if state == _INITIAL_STATE:
+                transform_line = get_diff_converter(line)
+            line = transform_line(line)
+
+            file_declaration = match(r"^Index: (?P<FilePath>.+)", line)
+            if file_declaration:
+                filename = file_declaration.group('FilePath')
+                current_file = DiffFile(filename)
+                files[filename] = current_file
+                state = _DECLARED_FILE_PATH
+                continue
+
+            lines_changed = match(r"^@@ -(?P<OldStartLine>\d+)(,\d+)? \+(?P<NewStartLine>\d+)(,\d+)? @@", line)
+            if lines_changed:
+                if state != _DECLARED_FILE_PATH and state != _PROCESSING_CHUNK:
+                    _log.error('Unexpected line change without file path '
+                               'declaration: %r' % line)
+                old_diff_line = int(lines_changed.group('OldStartLine'))
+                new_diff_line = int(lines_changed.group('NewStartLine'))
+                state = _PROCESSING_CHUNK
+                continue
+
+            if state == _PROCESSING_CHUNK:
+                if line.startswith('+'):
+                    current_file.add_new_line(new_diff_line, line[1:])
+                    new_diff_line += 1
+                elif line.startswith('-'):
+                    current_file.add_deleted_line(old_diff_line, line[1:])
+                    old_diff_line += 1
+                elif line.startswith(' '):
+                    current_file.add_unchanged_line(old_diff_line, new_diff_line, line[1:])
+                    old_diff_line += 1
+                    new_diff_line += 1
+                elif line == '\\ No newline at end of file':
+                    # Nothing to do.  We may still have some added lines.
+                    pass
+                else:
+                    _log.error('Unexpected diff format when parsing a '
+                               'chunk: %r' % line)
+        return files
diff --git a/Tools/Scripts/webkitpy/common/checkout/diff_parser_unittest.py b/Tools/Scripts/webkitpy/common/checkout/diff_parser_unittest.py
new file mode 100644
index 0000000..d61a098
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/diff_parser_unittest.py
@@ -0,0 +1,94 @@
+# Copyright (C) 2009 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 diff_parser
+import re
+
+from webkitpy.common.checkout.diff_test_data import DIFF_TEST_DATA
+
+class DiffParserTest(unittest.TestCase):
+    def test_diff_parser(self, parser = None):
+        if not parser:
+            parser = diff_parser.DiffParser(DIFF_TEST_DATA.splitlines())
+        self.assertEquals(3, len(parser.files))
+
+        self.assertTrue('WebCore/rendering/style/StyleFlexibleBoxData.h' in parser.files)
+        diff = parser.files['WebCore/rendering/style/StyleFlexibleBoxData.h']
+        self.assertEquals(7, len(diff.lines))
+        # The first two unchaged lines.
+        self.assertEquals((47, 47), diff.lines[0][0:2])
+        self.assertEquals('', diff.lines[0][2])
+        self.assertEquals((48, 48), diff.lines[1][0:2])
+        self.assertEquals('    unsigned align : 3; // EBoxAlignment', diff.lines[1][2])
+        # The deleted line
+        self.assertEquals((50, 0), diff.lines[3][0:2])
+        self.assertEquals('    unsigned orient: 1; // EBoxOrient', diff.lines[3][2])
+
+        # The first file looks OK. Let's check the next, more complicated file.
+        self.assertTrue('WebCore/rendering/style/StyleRareInheritedData.cpp' in parser.files)
+        diff = parser.files['WebCore/rendering/style/StyleRareInheritedData.cpp']
+        # There are 3 chunks.
+        self.assertEquals(7 + 7 + 9, len(diff.lines))
+        # Around an added line.
+        self.assertEquals((60, 61), diff.lines[9][0:2])
+        self.assertEquals((0, 62), diff.lines[10][0:2])
+        self.assertEquals((61, 63), diff.lines[11][0:2])
+        # Look through the last chunk, which contains both add's and delete's.
+        self.assertEquals((81, 83), diff.lines[14][0:2])
+        self.assertEquals((82, 84), diff.lines[15][0:2])
+        self.assertEquals((83, 85), diff.lines[16][0:2])
+        self.assertEquals((84, 0), diff.lines[17][0:2])
+        self.assertEquals((0, 86), diff.lines[18][0:2])
+        self.assertEquals((0, 87), diff.lines[19][0:2])
+        self.assertEquals((85, 88), diff.lines[20][0:2])
+        self.assertEquals((86, 89), diff.lines[21][0:2])
+        self.assertEquals((87, 90), diff.lines[22][0:2])
+
+        # Check if a newly added file is correctly handled.
+        diff = parser.files['LayoutTests/platform/mac/fast/flexbox/box-orient-button-expected.checksum']
+        self.assertEquals(1, len(diff.lines))
+        self.assertEquals((0, 1), diff.lines[0][0:2])
+
+    def test_git_mnemonicprefix(self):
+        p = re.compile(r' ([a|b])/')
+
+        prefixes = [
+            { 'a' : 'i', 'b' : 'w' }, # git-diff (compares the (i)ndex and the (w)ork tree)
+            { 'a' : 'c', 'b' : 'w' }, # git-diff HEAD (compares a (c)ommit and the (w)ork tree)
+            { 'a' : 'c', 'b' : 'i' }, # git diff --cached (compares a (c)ommit and the (i)ndex)
+            { 'a' : 'o', 'b' : 'w' }, # git-diff HEAD:file1 file2 (compares an (o)bject and a (w)ork tree entity)
+            { 'a' : '1', 'b' : '2' }, # git diff --no-index a b (compares two non-git things (1) and (2))
+        ]
+
+        for prefix in prefixes:
+            patch = p.sub(lambda x: " %s/" % prefix[x.group(1)], DIFF_TEST_DATA)
+            self.test_diff_parser(diff_parser.DiffParser(patch.splitlines()))
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/common/checkout/diff_test_data.py b/Tools/Scripts/webkitpy/common/checkout/diff_test_data.py
new file mode 100644
index 0000000..5f1719d
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/diff_test_data.py
@@ -0,0 +1,80 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#  FIXME: Store this as a .patch file in some new fixtures directory or similar.
+DIFF_TEST_DATA = '''diff --git a/WebCore/rendering/style/StyleFlexibleBoxData.h b/WebCore/rendering/style/StyleFlexibleBoxData.h
+index f5d5e74..3b6aa92 100644
+--- a/WebCore/rendering/style/StyleFlexibleBoxData.h
++++ b/WebCore/rendering/style/StyleFlexibleBoxData.h
+@@ -47,7 +47,6 @@ public:
+ 
+     unsigned align : 3; // EBoxAlignment
+     unsigned pack: 3; // EBoxAlignment
+-    unsigned orient: 1; // EBoxOrient
+     unsigned lines : 1; // EBoxLines
+ 
+ private:
+diff --git a/WebCore/rendering/style/StyleRareInheritedData.cpp b/WebCore/rendering/style/StyleRareInheritedData.cpp
+index ce21720..324929e 100644
+--- a/WebCore/rendering/style/StyleRareInheritedData.cpp
++++ b/WebCore/rendering/style/StyleRareInheritedData.cpp
+@@ -39,6 +39,7 @@ StyleRareInheritedData::StyleRareInheritedData()
+     , textSizeAdjust(RenderStyle::initialTextSizeAdjust())
+     , resize(RenderStyle::initialResize())
+     , userSelect(RenderStyle::initialUserSelect())
++    , boxOrient(RenderStyle::initialBoxOrient())
+ {
+ }
+ 
+@@ -58,6 +59,7 @@ StyleRareInheritedData::StyleRareInheritedData(const StyleRareInheritedData& o)
+     , textSizeAdjust(o.textSizeAdjust)
+     , resize(o.resize)
+     , userSelect(o.userSelect)
++    , boxOrient(o.boxOrient)
+ {
+ }
+ 
+@@ -81,7 +83,8 @@ bool StyleRareInheritedData::operator==(const StyleRareInheritedData& o) const
+         && khtmlLineBreak == o.khtmlLineBreak
+         && textSizeAdjust == o.textSizeAdjust
+         && resize == o.resize
+-        && userSelect == o.userSelect;
++        && userSelect == o.userSelect
++        && boxOrient == o.boxOrient;
+ }
+ 
+ bool StyleRareInheritedData::shadowDataEquivalent(const StyleRareInheritedData& o) const
+diff --git a/LayoutTests/platform/mac/fast/flexbox/box-orient-button-expected.checksum b/LayoutTests/platform/mac/fast/flexbox/box-orient-button-expected.checksum
+new file mode 100644
+index 0000000..6db26bd
+--- /dev/null
++++ b/LayoutTests/platform/mac/fast/flexbox/box-orient-button-expected.checksum
+@@ -0,0 +1 @@
++61a373ee739673a9dcd7bac62b9f182e
+\ No newline at end of file
+'''
diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/__init__.py b/Tools/Scripts/webkitpy/common/checkout/scm/__init__.py
new file mode 100644
index 0000000..f691f58
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/scm/__init__.py
@@ -0,0 +1,8 @@
+# Required for Python to search this directory for module files
+
+# We only export public API here.
+from .commitmessage import CommitMessage
+from .detection import SCMDetector
+from .git import Git, AmbiguousCommitError
+from .scm import SCM, AuthenticationError, CheckoutNeedsUpdate
+from .svn import SVN
diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/commitmessage.py b/Tools/Scripts/webkitpy/common/checkout/scm/commitmessage.py
new file mode 100644
index 0000000..be0d431
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/scm/commitmessage.py
@@ -0,0 +1,62 @@
+# Copyright (c) 2009, 2010, 2011 Google Inc. All rights reserved.
+# Copyright (c) 2009 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:
+#
+#     * 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 re
+
+
+def _first_non_empty_line_after_index(lines, index=0):
+    first_non_empty_line = index
+    for line in lines[index:]:
+        if re.match("^\s*$", line):
+            first_non_empty_line += 1
+        else:
+            break
+    return first_non_empty_line
+
+
+class CommitMessage:
+    def __init__(self, message):
+        self.message_lines = message[_first_non_empty_line_after_index(message, 0):]
+
+    def body(self, lstrip=False):
+        lines = self.message_lines[_first_non_empty_line_after_index(self.message_lines, 1):]
+        if lstrip:
+            lines = [line.lstrip() for line in lines]
+        return "\n".join(lines) + "\n"
+
+    def description(self, lstrip=False, strip_url=False):
+        line = self.message_lines[0]
+        if lstrip:
+            line = line.lstrip()
+        if strip_url:
+            line = re.sub("^(\s*)<.+> ", "\1", line)
+        return line
+
+    def message(self):
+        return "\n".join(self.message_lines) + "\n"
diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/detection.py b/Tools/Scripts/webkitpy/common/checkout/scm/detection.py
new file mode 100644
index 0000000..44bc926
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/scm/detection.py
@@ -0,0 +1,81 @@
+# Copyright (c) 2009, 2010, 2011 Google Inc. All rights reserved.
+# Copyright (c) 2009 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:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.system.filesystem import FileSystem
+from webkitpy.common.system.executive import Executive
+
+from webkitpy.common.system.deprecated_logging import log
+
+from .svn import SVN
+from .git import Git
+
+
+class SCMDetector(object):
+    def __init__(self, filesystem, executive):
+        self._filesystem = filesystem
+        self._executive = executive
+
+    def default_scm(self, patch_directories=None):
+        """Return the default SCM object as determined by the CWD and running code.
+
+        Returns the default SCM object for the current working directory; if the
+        CWD is not in a checkout, then we attempt to figure out if the SCM module
+        itself is part of a checkout, and return that one. If neither is part of
+        a checkout, None is returned.
+        """
+        cwd = self._filesystem.getcwd()
+        scm_system = self.detect_scm_system(cwd, patch_directories)
+        if not scm_system:
+            script_directory = self._filesystem.dirname(self._filesystem.path_to_module(self.__module__))
+            scm_system = self.detect_scm_system(script_directory, patch_directories)
+            if scm_system:
+                log("The current directory (%s) is not a WebKit checkout, using %s" % (cwd, scm_system.checkout_root))
+            else:
+                raise Exception("FATAL: Failed to determine the SCM system for either %s or %s" % (cwd, script_directory))
+        return scm_system
+
+    def detect_scm_system(self, path, patch_directories=None):
+        absolute_path = self._filesystem.abspath(path)
+
+        if patch_directories == []:
+            patch_directories = None
+
+        if SVN.in_working_directory(absolute_path, executive=self._executive):
+            return SVN(cwd=absolute_path, patch_directories=patch_directories, filesystem=self._filesystem, executive=self._executive)
+
+        if Git.in_working_directory(absolute_path, executive=self._executive):
+            return Git(cwd=absolute_path, filesystem=self._filesystem, executive=self._executive)
+
+        return None
+
+
+# FIXME: These free functions are all deprecated:
+
+def detect_scm_system(path, patch_directories=None):
+    return SCMDetector(FileSystem(), Executive()).detect_scm_system(path, patch_directories)
diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/detection_unittest.py b/Tools/Scripts/webkitpy/common/checkout/scm/detection_unittest.py
new file mode 100644
index 0000000..ecd9125
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/scm/detection_unittest.py
@@ -0,0 +1,48 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2009 Apple Inc. All rights reserved.
+# Copyright (C) 2011 Daniel Bates (dbates@intudata.com). 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 .detection import SCMDetector
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.executive_mock import MockExecutive
+from webkitpy.common.system.outputcapture import OutputCapture
+
+
+class SCMDetectorTest(unittest.TestCase):
+    def test_detect_scm_system(self):
+        filesystem = MockFileSystem()
+        executive = MockExecutive(should_log=True)
+        detector = SCMDetector(filesystem, executive)
+
+        expected_stderr = "MOCK run_command: ['svn', 'info'], cwd=/\nMOCK run_command: ['git', 'rev-parse', '--is-inside-work-tree'], cwd=/\n"
+        scm = OutputCapture().assert_outputs(self, detector.detect_scm_system, ["/"], expected_stderr=expected_stderr)
+        self.assertEqual(scm, None)
+        # FIXME: This should make a synthetic tree and test SVN and Git detection in that tree.
diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/git.py b/Tools/Scripts/webkitpy/common/checkout/scm/git.py
new file mode 100644
index 0000000..f688238
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/scm/git.py
@@ -0,0 +1,496 @@
+# Copyright (c) 2009, 2010, 2011 Google Inc. All rights reserved.
+# Copyright (c) 2009 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:
+#
+#     * 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 logging
+import os
+import re
+
+from webkitpy.common.memoized import memoized
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.common.system.executive import Executive, ScriptError
+
+from .commitmessage import CommitMessage
+from .scm import AuthenticationError, SCM, commit_error_handler
+from .svn import SVN, SVNRepository
+
+
+_log = logging.getLogger(__name__)
+
+
+def run_command(*args, **kwargs):
+    # FIXME: This should not be a global static.
+    # New code should use Executive.run_command directly instead
+    return Executive().run_command(*args, **kwargs)
+
+
+class AmbiguousCommitError(Exception):
+    def __init__(self, num_local_commits, working_directory_is_clean):
+        self.num_local_commits = num_local_commits
+        self.working_directory_is_clean = working_directory_is_clean
+
+
+class Git(SCM, SVNRepository):
+
+    # Git doesn't appear to document error codes, but seems to return
+    # 1 or 128, mostly.
+    ERROR_FILE_IS_MISSING = 128
+
+    executable_name = 'git'
+
+    def __init__(self, cwd, **kwargs):
+        SCM.__init__(self, cwd, **kwargs)
+        self._check_git_architecture()
+
+    def _machine_is_64bit(self):
+        import platform
+        # This only is tested on Mac.
+        if not platform.mac_ver()[0]:
+            return False
+
+        # platform.architecture()[0] can be '64bit' even if the machine is 32bit:
+        # http://mail.python.org/pipermail/pythonmac-sig/2009-September/021648.html
+        # Use the sysctl command to find out what the processor actually supports.
+        return self.run(['sysctl', '-n', 'hw.cpu64bit_capable']).rstrip() == '1'
+
+    def _executable_is_64bit(self, path):
+        # Again, platform.architecture() fails us.  On my machine
+        # git_bits = platform.architecture(executable=git_path, bits='default')[0]
+        # git_bits is just 'default', meaning the call failed.
+        file_output = self.run(['file', path])
+        return re.search('x86_64', file_output)
+
+    def _check_git_architecture(self):
+        if not self._machine_is_64bit():
+            return
+
+        # We could path-search entirely in python or with
+        # which.py (http://code.google.com/p/which), but this is easier:
+        git_path = self.run(['which', self.executable_name]).rstrip()
+        if self._executable_is_64bit(git_path):
+            return
+
+        webkit_dev_thread_url = "https://lists.webkit.org/pipermail/webkit-dev/2010-December/015287.html"
+        log("Warning: This machine is 64-bit, but the git binary (%s) does not support 64-bit.\nInstall a 64-bit git for better performance, see:\n%s\n" % (git_path, webkit_dev_thread_url))
+
+    def _run_git(self, command_args, **kwargs):
+        full_command_args = [self.executable_name] + command_args
+        full_kwargs = kwargs
+        if not 'cwd' in full_kwargs:
+            full_kwargs['cwd'] = self.checkout_root
+        return self.run(full_command_args, **full_kwargs)
+
+    @classmethod
+    def in_working_directory(cls, path, executive=None):
+        try:
+            executive = executive or Executive()
+            return executive.run_command([cls.executable_name, 'rev-parse', '--is-inside-work-tree'], cwd=path, error_handler=Executive.ignore_error).rstrip() == "true"
+        except OSError, e:
+            # The Windows bots seem to through a WindowsError when git isn't installed.
+            return False
+
+    def find_checkout_root(self, path):
+        # "git rev-parse --show-cdup" would be another way to get to the root
+        checkout_root = self._run_git(['rev-parse', '--show-toplevel'], cwd=(path or "./")).strip()
+        if not self._filesystem.isabs(checkout_root):  # Sometimes git returns relative paths
+            checkout_root = self._filesystem.join(path, checkout_root)
+        return checkout_root
+
+    def to_object_name(self, filepath):
+        # FIXME: This can't be the right way to append a slash.
+        root_end_with_slash = self._filesystem.join(self.find_checkout_root(self._filesystem.dirname(filepath)), '')
+        # FIXME: This seems to want some sort of rel_path instead?
+        return filepath.replace(root_end_with_slash, '')
+
+    @classmethod
+    def read_git_config(cls, key, cwd=None):
+        # FIXME: This should probably use cwd=self.checkout_root.
+        # Pass --get-all for cases where the config has multiple values
+        # Pass the cwd if provided so that we can handle the case of running webkit-patch outside of the working directory.
+        # FIXME: This should use an Executive.
+        return run_command([cls.executable_name, "config", "--get-all", key], error_handler=Executive.ignore_error, cwd=cwd).rstrip('\n')
+
+    @staticmethod
+    def commit_success_regexp():
+        return "^Committed r(?P<svn_revision>\d+)$"
+
+    def discard_local_commits(self):
+        self._run_git(['reset', '--hard', self.remote_branch_ref()])
+
+    def local_commits(self):
+        return self._run_git(['log', '--pretty=oneline', 'HEAD...' + self.remote_branch_ref()]).splitlines()
+
+    def rebase_in_progress(self):
+        return self._filesystem.exists(self.absolute_path(self._filesystem.join('.git', 'rebase-apply')))
+
+    def working_directory_is_clean(self):
+        return self._run_git(['diff', 'HEAD', '--no-renames', '--name-only']) == ""
+
+    def clean_working_directory(self):
+        # Could run git clean here too, but that wouldn't match working_directory_is_clean
+        self._run_git(['reset', '--hard', 'HEAD'])
+        # Aborting rebase even though this does not match working_directory_is_clean
+        if self.rebase_in_progress():
+            self._run_git(['rebase', '--abort'])
+
+    def status_command(self):
+        # git status returns non-zero when there are changes, so we use git diff name --name-status HEAD instead.
+        # No file contents printed, thus utf-8 autodecoding in self.run is fine.
+        return [self.executable_name, "diff", "--name-status", "--no-renames", "HEAD"]
+
+    def _status_regexp(self, expected_types):
+        return '^(?P<status>[%s])\t(?P<filename>.+)$' % expected_types
+
+    def add_list(self, paths, return_exit_code=False):
+        return self._run_git(["add"] + paths, return_exit_code=return_exit_code)
+
+    def delete_list(self, paths):
+        return self._run_git(["rm", "-f"] + paths)
+
+    def exists(self, path):
+        return_code = self._run_git(["show", "HEAD:%s" % path], return_exit_code=True, decode_output=False)
+        return return_code != self.ERROR_FILE_IS_MISSING
+
+    def _branch_from_ref(self, ref):
+        return ref.replace('refs/heads/', '')
+
+    def _current_branch(self):
+        return self._branch_from_ref(self._run_git(['symbolic-ref', '-q', 'HEAD']).strip())
+
+    def _upstream_branch(self):
+        current_branch = self._current_branch()
+        return self._branch_from_ref(self.read_git_config('branch.%s.merge' % current_branch, cwd=self.checkout_root).strip())
+
+    def merge_base(self, git_commit):
+        if git_commit:
+            # Rewrite UPSTREAM to the upstream branch
+            if 'UPSTREAM' in git_commit:
+                upstream = self._upstream_branch()
+                if not upstream:
+                    raise ScriptError(message='No upstream/tracking branch set.')
+                git_commit = git_commit.replace('UPSTREAM', upstream)
+
+            # Special-case <refname>.. to include working copy changes, e.g., 'HEAD....' shows only the diffs from HEAD.
+            if git_commit.endswith('....'):
+                return git_commit[:-4]
+
+            if '..' not in git_commit:
+                git_commit = git_commit + "^.." + git_commit
+            return git_commit
+
+        return self.remote_merge_base()
+
+    def changed_files(self, git_commit=None):
+        # FIXME: --diff-filter could be used to avoid the "extract_filenames" step.
+        status_command = [self.executable_name, 'diff', '-r', '--name-status', "--no-renames", "--no-ext-diff", "--full-index", self.merge_base(git_commit)]
+        # FIXME: I'm not sure we're returning the same set of files that SVN.changed_files is.
+        # Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R)
+        return self.run_status_and_extract_filenames(status_command, self._status_regexp("ADM"))
+
+    def _changes_files_for_commit(self, git_commit):
+        # --pretty="format:" makes git show not print the commit log header,
+        changed_files = self._run_git(["show", "--pretty=format:", "--name-only", git_commit]).splitlines()
+        # instead it just prints a blank line at the top, so we skip the blank line:
+        return changed_files[1:]
+
+    def changed_files_for_revision(self, revision):
+        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):
+        # raise a script error if path does not exists to match the behavior of  the svn implementation.
+        if not self._filesystem.exists(path):
+            raise ScriptError(message="Path %s does not exist." % path)
+
+        # git rev-list head --remove-empty --limit=5 -- path would be equivalent.
+        commit_ids = self._run_git(["log", "--remove-empty", "--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.
+        status_command = [self.executable_name, 'diff', '--name-status', '--no-renames', '--diff-filter=U']
+        return self.run_status_and_extract_filenames(status_command, self._status_regexp("U"))
+
+    def added_files(self):
+        return self.run_status_and_extract_filenames(self.status_command(), self._status_regexp("A"))
+
+    def deleted_files(self):
+        return self.run_status_and_extract_filenames(self.status_command(), self._status_regexp("D"))
+
+    @staticmethod
+    def supports_local_commits():
+        return True
+
+    def display_name(self):
+        return "git"
+
+    def svn_revision(self, path):
+        _log.debug('Running git.head_svn_revision... (Temporary logging message)')
+        git_log = self._run_git(['log', '-25', path])
+        match = re.search("^\s*git-svn-id:.*@(?P<svn_revision>\d+)\ ", git_log, re.MULTILINE)
+        if not match:
+            return ""
+        return str(match.group('svn_revision'))
+
+    def prepend_svn_revision(self, diff):
+        revision = self.head_svn_revision()
+        if not revision:
+            return diff
+
+        return "Subversion Revision: " + revision + '\n' + diff
+
+    def create_patch(self, git_commit=None, changed_files=None):
+        """Returns a byte array (str()) representing the patch file.
+        Patch files are effectively binary since they may contain
+        files of multiple different encodings."""
+
+        # Put code changes at the top of the patch and layout tests
+        # at the bottom, this makes for easier reviewing.
+        config_path = self._filesystem.dirname(self._filesystem.path_to_module('webkitpy.common.config'))
+        order_file = self._filesystem.join(config_path, 'orderfile')
+        order = ""
+        if self._filesystem.exists(order_file):
+            order = "-O%s" % order_file
+
+        command = [self.executable_name, 'diff', '--binary', '--no-color', "--no-ext-diff", "--full-index", "--no-renames", order, self.merge_base(git_commit), "--"]
+        if changed_files:
+            command += changed_files
+        return self.prepend_svn_revision(self.run(command, decode_output=False, cwd=self.checkout_root))
+
+    def _run_git_svn_find_rev(self, arg):
+        # git svn find-rev always exits 0, even when the revision or commit is not found.
+        return self._run_git(['svn', 'find-rev', arg]).rstrip()
+
+    def _string_to_int_or_none(self, string):
+        try:
+            return int(string)
+        except ValueError, e:
+            return None
+
+    @memoized
+    def git_commit_from_svn_revision(self, svn_revision):
+        git_commit = self._run_git_svn_find_rev('r%s' % svn_revision)
+        if not git_commit:
+            # FIXME: Alternatively we could offer to update the checkout? Or return None?
+            raise ScriptError(message='Failed to find git commit for revision %s, your checkout likely needs an update.' % svn_revision)
+        return git_commit
+
+    @memoized
+    def svn_revision_from_git_commit(self, git_commit):
+        svn_revision = self._run_git_svn_find_rev(git_commit)
+        return self._string_to_int_or_none(svn_revision)
+
+    def contents_at_revision(self, path, revision):
+        """Returns a byte array (str()) containing the contents
+        of path @ revision in the repository."""
+        return self._run_git(["show", "%s:%s" % (self.git_commit_from_svn_revision(revision), path)], decode_output=False)
+
+    def diff_for_revision(self, revision):
+        git_commit = self.git_commit_from_svn_revision(revision)
+        return self.create_patch(git_commit)
+
+    def diff_for_file(self, path, log=None):
+        return self._run_git(['diff', 'HEAD', '--no-renames', '--', path])
+
+    def show_head(self, path):
+        return self._run_git(['show', 'HEAD:' + self.to_object_name(path)], decode_output=False)
+
+    def committer_email_for_revision(self, revision):
+        git_commit = self.git_commit_from_svn_revision(revision)
+        committer_email = self._run_git(["log", "-1", "--pretty=format:%ce", git_commit])
+        # Git adds an extra @repository_hash to the end of every committer email, remove it:
+        return committer_email.rsplit("@", 1)[0]
+
+    def apply_reverse_diff(self, revision):
+        # Assume the revision is an svn revision.
+        git_commit = self.git_commit_from_svn_revision(revision)
+        # I think this will always fail due to ChangeLogs.
+        self._run_git(['revert', '--no-commit', git_commit], error_handler=Executive.ignore_error)
+
+    def revert_files(self, file_paths):
+        self._run_git(['checkout', 'HEAD'] + file_paths)
+
+    def _assert_can_squash(self, working_directory_is_clean):
+        squash = Git.read_git_config('webkit-patch.commit-should-always-squash', cwd=self.checkout_root)
+        should_squash = squash and squash.lower() == "true"
+
+        if not should_squash:
+            # Only warn if there are actually multiple commits to squash.
+            num_local_commits = len(self.local_commits())
+            if num_local_commits > 1 or (num_local_commits > 0 and not working_directory_is_clean):
+                raise AmbiguousCommitError(num_local_commits, working_directory_is_clean)
+
+    def commit_with_message(self, message, username=None, password=None, git_commit=None, force_squash=False, changed_files=None):
+        # Username is ignored during Git commits.
+        working_directory_is_clean = self.working_directory_is_clean()
+
+        if git_commit:
+            # Special-case HEAD.. to mean working-copy changes only.
+            if git_commit.upper() == 'HEAD..':
+                if working_directory_is_clean:
+                    raise ScriptError(message="The working copy is not modified. --git-commit=HEAD.. only commits working copy changes.")
+                self.commit_locally_with_message(message)
+                return self._commit_on_branch(message, 'HEAD', username=username, password=password)
+
+            # Need working directory changes to be committed so we can checkout the merge branch.
+            if not working_directory_is_clean:
+                # FIXME: webkit-patch land will modify the ChangeLogs to correct the reviewer.
+                # That will modify the working-copy and cause us to hit this error.
+                # The ChangeLog modification could be made to modify the existing local commit.
+                raise ScriptError(message="Working copy is modified. Cannot commit individual git_commits.")
+            return self._commit_on_branch(message, git_commit, username=username, password=password)
+
+        if not force_squash:
+            self._assert_can_squash(working_directory_is_clean)
+        self._run_git(['reset', '--soft', self.remote_merge_base()])
+        self.commit_locally_with_message(message)
+        return self.push_local_commits_to_server(username=username, password=password)
+
+    def _commit_on_branch(self, message, git_commit, username=None, password=None):
+        branch_name = self._current_branch()
+        commit_ids = self.commit_ids_from_commitish_arguments([git_commit])
+
+        # We want to squash all this branch's commits into one commit with the proper description.
+        # We do this by doing a "merge --squash" into a new commit branch, then dcommitting that.
+        MERGE_BRANCH_NAME = 'webkit-patch-land'
+        self.delete_branch(MERGE_BRANCH_NAME)
+
+        # We might be in a directory that's present in this branch but not in the
+        # trunk.  Move up to the top of the tree so that git commands that expect a
+        # valid CWD won't fail after we check out the merge branch.
+        # FIXME: We should never be using chdir! We can instead pass cwd= to run_command/self.run!
+        self._filesystem.chdir(self.checkout_root)
+
+        # Stuff our change into the merge branch.
+        # We wrap in a try...finally block so if anything goes wrong, we clean up the branches.
+        commit_succeeded = True
+        try:
+            self._run_git(['checkout', '-q', '-b', MERGE_BRANCH_NAME, self.remote_branch_ref()])
+
+            for commit in commit_ids:
+                # We're on a different branch now, so convert "head" to the branch name.
+                commit = re.sub(r'(?i)head', branch_name, commit)
+                # FIXME: Once changed_files and create_patch are modified to separately handle each
+                # commit in a commit range, commit each cherry pick so they'll get dcommitted separately.
+                self._run_git(['cherry-pick', '--no-commit', commit])
+
+            self._run_git(['commit', '-m', message])
+            output = self.push_local_commits_to_server(username=username, password=password)
+        except Exception, e:
+            log("COMMIT FAILED: " + str(e))
+            output = "Commit failed."
+            commit_succeeded = False
+        finally:
+            # And then swap back to the original branch and clean up.
+            self.clean_working_directory()
+            self._run_git(['checkout', '-q', branch_name])
+            self.delete_branch(MERGE_BRANCH_NAME)
+
+        return output
+
+    def svn_commit_log(self, svn_revision):
+        svn_revision = self.strip_r_from_svn_revision(svn_revision)
+        return self._run_git(['svn', 'log', '-r', svn_revision])
+
+    def last_svn_commit_log(self):
+        return self._run_git(['svn', 'log', '--limit=1'])
+
+    def svn_blame(self, path):
+        return self._run_git(['svn', 'blame', path])
+
+    # Git-specific methods:
+    def _branch_ref_exists(self, branch_ref):
+        return self._run_git(['show-ref', '--quiet', '--verify', branch_ref], return_exit_code=True) == 0
+
+    def delete_branch(self, branch_name):
+        if self._branch_ref_exists('refs/heads/' + branch_name):
+            self._run_git(['branch', '-D', branch_name])
+
+    def remote_merge_base(self):
+        return self._run_git(['merge-base', self.remote_branch_ref(), 'HEAD']).strip()
+
+    def remote_branch_ref(self):
+        # Use references so that we can avoid collisions, e.g. we don't want to operate on refs/heads/trunk if it exists.
+        remote_branch_refs = Git.read_git_config('svn-remote.svn.fetch', cwd=self.checkout_root)
+        if not remote_branch_refs:
+            remote_master_ref = 'refs/remotes/origin/master'
+            if not self._branch_ref_exists(remote_master_ref):
+                raise ScriptError(message="Can't find a branch to diff against. svn-remote.svn.fetch is not in the git config and %s does not exist" % remote_master_ref)
+            return remote_master_ref
+
+        # FIXME: What's the right behavior when there are multiple svn-remotes listed?
+        # For now, just use the first one.
+        first_remote_branch_ref = remote_branch_refs.split('\n')[0]
+        return first_remote_branch_ref.split(':')[1]
+
+    def commit_locally_with_message(self, message):
+        self._run_git(['commit', '--all', '-F', '-'], input=message)
+
+    def push_local_commits_to_server(self, username=None, password=None):
+        dcommit_command = ['svn', 'dcommit']
+        if (not username or not password) and not self.has_authorization_for_realm(SVN.svn_server_realm):
+            raise AuthenticationError(SVN.svn_server_host, prompt_for_password=True)
+        if username:
+            dcommit_command.extend(["--username", username])
+        output = self._run_git(dcommit_command, error_handler=commit_error_handler, input=password)
+        return output
+
+    # This function supports the following argument formats:
+    # no args : rev-list trunk..HEAD
+    # A..B    : rev-list A..B
+    # A...B   : error!
+    # A B     : [A, B]  (different from git diff, which would use "rev-list A..B")
+    def commit_ids_from_commitish_arguments(self, args):
+        if not len(args):
+            args.append('%s..HEAD' % self.remote_branch_ref())
+
+        commit_ids = []
+        for commitish in args:
+            if '...' in commitish:
+                raise ScriptError(message="'...' is not supported (found in '%s'). Did you mean '..'?" % commitish)
+            elif '..' in commitish:
+                commit_ids += reversed(self._run_git(['rev-list', commitish]).splitlines())
+            else:
+                # Turn single commits or branch or tag names into commit ids.
+                commit_ids += self._run_git(['rev-parse', '--revs-only', commitish]).splitlines()
+        return commit_ids
+
+    def commit_message_for_local_commit(self, commit_id):
+        commit_lines = self._run_git(['cat-file', 'commit', commit_id]).splitlines()
+
+        # Skip the git headers.
+        first_line_after_headers = 0
+        for line in commit_lines:
+            first_line_after_headers += 1
+            if line == "":
+                break
+        return CommitMessage(commit_lines[first_line_after_headers:])
+
+    def files_changed_summary_for_commit(self, commit_id):
+        return self._run_git(['diff-tree', '--shortstat', '--no-renames', '--no-commit-id', commit_id])
diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/scm.py b/Tools/Scripts/webkitpy/common/checkout/scm/scm.py
new file mode 100644
index 0000000..815e750
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/scm/scm.py
@@ -0,0 +1,247 @@
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 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:
+#
+#     * 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.
+#
+# Python module for interacting with an SCM system (like SVN or Git)
+
+import logging
+import re
+
+from webkitpy.common.system.deprecated_logging import error, log
+from webkitpy.common.system.executive import Executive, ScriptError
+from webkitpy.common.system.filesystem import FileSystem
+
+
+class CheckoutNeedsUpdate(ScriptError):
+    def __init__(self, script_args, exit_code, output, cwd):
+        ScriptError.__init__(self, script_args=script_args, exit_code=exit_code, output=output, cwd=cwd)
+
+
+# FIXME: Should be moved onto SCM
+def commit_error_handler(error):
+    if re.search("resource out of date", error.output):
+        raise CheckoutNeedsUpdate(script_args=error.script_args, exit_code=error.exit_code, output=error.output, cwd=error.cwd)
+    Executive.default_error_handler(error)
+
+
+class AuthenticationError(Exception):
+    def __init__(self, server_host, prompt_for_password=False):
+        self.server_host = server_host
+        self.prompt_for_password = prompt_for_password
+
+
+
+# SCM methods are expected to return paths relative to self.checkout_root.
+class SCM:
+    def __init__(self, cwd, executive=None, filesystem=None):
+        self.cwd = cwd
+        self._executive = executive or Executive()
+        self._filesystem = filesystem or FileSystem()
+        self.checkout_root = self.find_checkout_root(self.cwd)
+
+    # A wrapper used by subclasses to create processes.
+    def run(self, args, cwd=None, input=None, error_handler=None, return_exit_code=False, return_stderr=True, decode_output=True):
+        # FIXME: We should set cwd appropriately.
+        return self._executive.run_command(args,
+                           cwd=cwd,
+                           input=input,
+                           error_handler=error_handler,
+                           return_exit_code=return_exit_code,
+                           return_stderr=return_stderr,
+                           decode_output=decode_output)
+
+    # SCM always returns repository relative path, but sometimes we need
+    # absolute paths to pass to rm, etc.
+    def absolute_path(self, repository_relative_path):
+        return self._filesystem.join(self.checkout_root, repository_relative_path)
+
+    # FIXME: This belongs in Checkout, not SCM.
+    def scripts_directory(self):
+        return self._filesystem.join(self.checkout_root, "Tools", "Scripts")
+
+    # FIXME: This belongs in Checkout, not SCM.
+    def script_path(self, script_name):
+        return self._filesystem.join(self.scripts_directory(), script_name)
+
+    def ensure_clean_working_directory(self, force_clean):
+        if self.working_directory_is_clean():
+            return
+        if not force_clean:
+            print self.run(self.status_command(), error_handler=Executive.ignore_error, cwd=self.checkout_root)
+            raise ScriptError(message="Working directory has modifications, pass --force-clean or --no-clean to continue.")
+        log("Cleaning working directory")
+        self.clean_working_directory()
+
+    def ensure_no_local_commits(self, force):
+        if not self.supports_local_commits():
+            return
+        commits = self.local_commits()
+        if not len(commits):
+            return
+        if not force:
+            error("Working directory has local commits, pass --force-clean to continue.")
+        self.discard_local_commits()
+
+    def run_status_and_extract_filenames(self, status_command, status_regexp):
+        filenames = []
+        # We run with cwd=self.checkout_root so that returned-paths are root-relative.
+        for line in self.run(status_command, cwd=self.checkout_root).splitlines():
+            match = re.search(status_regexp, line)
+            if not match:
+                continue
+            # status = match.group('status')
+            filename = match.group('filename')
+            filenames.append(filename)
+        return filenames
+
+    def strip_r_from_svn_revision(self, svn_revision):
+        match = re.match("^r(?P<svn_revision>\d+)", unicode(svn_revision))
+        if (match):
+            return match.group('svn_revision')
+        return svn_revision
+
+    def svn_revision_from_commit_text(self, commit_text):
+        match = re.search(self.commit_success_regexp(), commit_text, re.MULTILINE)
+        return match.group('svn_revision')
+
+    @staticmethod
+    def _subclass_must_implement():
+        raise NotImplementedError("subclasses must implement")
+
+    @classmethod
+    def in_working_directory(cls, path, executive=None):
+        SCM._subclass_must_implement()
+
+    def find_checkout_root(path):
+        SCM._subclass_must_implement()
+
+    @staticmethod
+    def commit_success_regexp():
+        SCM._subclass_must_implement()
+
+    def working_directory_is_clean(self):
+        self._subclass_must_implement()
+
+    def clean_working_directory(self):
+        self._subclass_must_implement()
+
+    def status_command(self):
+        self._subclass_must_implement()
+
+    def add(self, path, return_exit_code=False):
+        self.add_list([path], return_exit_code)
+
+    def add_list(self, paths, return_exit_code=False):
+        self._subclass_must_implement()
+
+    def delete(self, path):
+        self.delete_list([path])
+
+    def delete_list(self, paths):
+        self._subclass_must_implement()
+
+    def exists(self, path):
+        self._subclass_must_implement()
+
+    def changed_files(self, git_commit=None):
+        self._subclass_must_implement()
+
+    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):
+        self._subclass_must_implement()
+
+    def conflicted_files(self):
+        self._subclass_must_implement()
+
+    def display_name(self):
+        self._subclass_must_implement()
+
+    def head_svn_revision(self):
+        return self.svn_revision(self.checkout_root)
+
+    def svn_revision(self, path):
+        self._subclass_must_implement()
+
+    def create_patch(self, git_commit=None, changed_files=None):
+        self._subclass_must_implement()
+
+    def committer_email_for_revision(self, revision):
+        self._subclass_must_implement()
+
+    def contents_at_revision(self, path, revision):
+        self._subclass_must_implement()
+
+    def diff_for_revision(self, revision):
+        self._subclass_must_implement()
+
+    def diff_for_file(self, path, log=None):
+        self._subclass_must_implement()
+
+    def show_head(self, path):
+        self._subclass_must_implement()
+
+    def apply_reverse_diff(self, revision):
+        self._subclass_must_implement()
+
+    def revert_files(self, file_paths):
+        self._subclass_must_implement()
+
+    def commit_with_message(self, message, username=None, password=None, git_commit=None, force_squash=False, changed_files=None):
+        self._subclass_must_implement()
+
+    def svn_commit_log(self, svn_revision):
+        self._subclass_must_implement()
+
+    def last_svn_commit_log(self):
+        self._subclass_must_implement()
+
+    def svn_blame(self, path):
+        self._subclass_must_implement()
+
+    # Subclasses must indicate if they support local commits,
+    # but the SCM baseclass will only call local_commits methods when this is true.
+    @staticmethod
+    def supports_local_commits():
+        SCM._subclass_must_implement()
+
+    def remote_merge_base():
+        SCM._subclass_must_implement()
+
+    def commit_locally_with_message(self, message):
+        error("Your source control manager does not support local commits.")
+
+    def discard_local_commits(self):
+        pass
+
+    def local_commits(self):
+        return []
diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/scm_mock.py b/Tools/Scripts/webkitpy/common/checkout/scm/scm_mock.py
new file mode 100644
index 0000000..9dd01e8
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/scm/scm_mock.py
@@ -0,0 +1,124 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.executive_mock import MockExecutive
+
+
+class MockSCM(object):
+    def __init__(self, filesystem=None, executive=None):
+        self.checkout_root = "/mock-checkout"
+        self.added_paths = set()
+        self._filesystem = filesystem or MockFileSystem()
+        self._executive = executive or MockExecutive()
+
+    def add(self, destination_path, return_exit_code=False):
+        self.add_list([destination_path], return_exit_code)
+
+    def add_list(self, destination_paths, return_exit_code=False):
+        self.added_paths.update(set(destination_paths))
+        if return_exit_code:
+            return 0
+
+    def ensure_clean_working_directory(self, force_clean):
+        pass
+
+    def supports_local_commits(self):
+        return True
+
+    def ensure_no_local_commits(self, force_clean):
+        pass
+
+    def exists(self, path):
+        # TestRealMain.test_real_main (and several other rebaseline tests) are sensitive to this return value.
+        # We should make those tests more robust, but for now we just return True always (since no test needs otherwise).
+        return True
+
+    def absolute_path(self, *comps):
+        return self._filesystem.join(self.checkout_root, *comps)
+
+    def changed_files(self, git_commit=None):
+        return ["MockFile1"]
+
+    def changed_files_for_revision(self, revision):
+        return ["MockFile1"]
+
+    def head_svn_revision(self):
+        return '1234'
+
+    def svn_revision(self, path):
+        return '5678'
+
+    def create_patch(self, git_commit, changed_files=None):
+        return "Patch1"
+
+    def commit_ids_from_commitish_arguments(self, args):
+        return ["Commitish1", "Commitish2"]
+
+    def committer_email_for_revision(self, revision):
+        return "mock@webkit.org"
+
+    def commit_locally_with_message(self, message):
+        pass
+
+    def commit_with_message(self, message, username=None, password=None, git_commit=None, force_squash=False, changed_files=None):
+        pass
+
+    def merge_base(self, git_commit):
+        return None
+
+    def commit_message_for_local_commit(self, commit_id):
+        if commit_id == "Commitish1":
+            return CommitMessage("CommitMessage1\n" \
+                "https://bugs.example.org/show_bug.cgi?id=50000\n")
+        if commit_id == "Commitish2":
+            return CommitMessage("CommitMessage2\n" \
+                "https://bugs.example.org/show_bug.cgi?id=50001\n")
+        raise Exception("Bogus commit_id in commit_message_for_local_commit.")
+
+    def diff_for_file(self, path, log=None):
+        return path + '-diff'
+
+    def diff_for_revision(self, revision):
+        return "DiffForRevision%s\nhttp://bugs.webkit.org/show_bug.cgi?id=12345" % revision
+
+    def show_head(self, path):
+        return path
+
+    def svn_revision_from_commit_text(self, commit_text):
+        return "49824"
+
+    def delete(self, path):
+        return self.delete_list([path])
+
+    def delete_list(self, paths):
+        if not self._filesystem:
+            return
+        for path in paths:
+            if self._filesystem.exists(path):
+                self._filesystem.remove(path)
diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/scm_unittest.py b/Tools/Scripts/webkitpy/common/checkout/scm/scm_unittest.py
new file mode 100644
index 0000000..802fe2c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/scm/scm_unittest.py
@@ -0,0 +1,1612 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2009 Apple Inc. All rights reserved.
+# Copyright (C) 2011 Daniel Bates (dbates@intudata.com). 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 atexit
+import base64
+import codecs
+import getpass
+import os
+import os.path
+import re
+import stat
+import sys
+import subprocess
+import tempfile
+import time
+import unittest
+import urllib
+import shutil
+
+from datetime import date
+from webkitpy.common.checkout.checkout import Checkout
+from webkitpy.common.config.committers import Committer  # FIXME: This should not be needed
+from webkitpy.common.net.bugzilla import Attachment # FIXME: This should not be needed
+from webkitpy.common.system.executive import Executive, ScriptError
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.system.executive_mock import MockExecutive
+
+from .git import Git, AmbiguousCommitError
+from .detection import detect_scm_system
+from .scm import SCM, CheckoutNeedsUpdate, commit_error_handler, AuthenticationError
+from .svn import SVN
+
+# We cache the mock SVN repo so that we don't create it again for each call to an SVNTest or GitTest test_ method.
+# We store it in a global variable so that we can delete this cached repo on exit(3).
+# FIXME: Remove this once we migrate to Python 2.7. Unittest in Python 2.7 supports module-specific setup and teardown functions.
+cached_svn_repo_path = None
+
+
+def remove_dir(path):
+    # Change directory to / to ensure that we aren't in the directory we want to delete.
+    os.chdir('/')
+    shutil.rmtree(path)
+
+
+# FIXME: Remove this once we migrate to Python 2.7. Unittest in Python 2.7 supports module-specific setup and teardown functions.
+@atexit.register
+def delete_cached_mock_repo_at_exit():
+    if cached_svn_repo_path:
+        remove_dir(cached_svn_repo_path)
+
+# Eventually we will want to write tests which work for both scms. (like update_webkit, changed_files, etc.)
+# Perhaps through some SCMTest base-class which both SVNTest and GitTest inherit from.
+
+def run_command(*args, **kwargs):
+    # FIXME: This should not be a global static.
+    # New code should use Executive.run_command directly instead
+    return Executive().run_command(*args, **kwargs)
+
+
+# FIXME: This should be unified into one of the executive.py commands!
+# Callers could use run_and_throw_if_fail(args, cwd=cwd, quiet=True)
+def run_silent(args, cwd=None):
+    # Note: Not thread safe: http://bugs.python.org/issue2320
+    process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
+    process.communicate() # ignore output
+    exit_code = process.wait()
+    if exit_code:
+        raise ScriptError('Failed to run "%s"  exit_code: %d  cwd: %s' % (args, exit_code, cwd))
+
+
+def write_into_file_at_path(file_path, contents, encoding="utf-8"):
+    if encoding:
+        with codecs.open(file_path, "w", encoding) as file:
+            file.write(contents)
+    else:
+        with open(file_path, "w") as file:
+            file.write(contents)
+
+
+def read_from_path(file_path, encoding="utf-8"):
+    with codecs.open(file_path, "r", encoding) as file:
+        return file.read()
+
+
+def _make_diff(command, *args):
+    # We use this wrapper to disable output decoding. diffs should be treated as
+    # binary files since they may include text files of multiple differnet encodings.
+    # FIXME: This should use an Executive.
+    return run_command([command, "diff"] + list(args), decode_output=False)
+
+
+def _svn_diff(*args):
+    return _make_diff("svn", *args)
+
+
+def _git_diff(*args):
+    return _make_diff("git", *args)
+
+
+# Exists to share svn repository creation code between the git and svn tests
+class SVNTestRepository:
+    @classmethod
+    def _svn_add(cls, path):
+        run_command(["svn", "add", path])
+
+    @classmethod
+    def _svn_commit(cls, message):
+        run_command(["svn", "commit", "--quiet", "--message", message])
+
+    @classmethod
+    def _setup_test_commits(cls, svn_repo_url):
+
+        svn_checkout_path = tempfile.mkdtemp(suffix="svn_test_checkout")
+        run_command(['svn', 'checkout', '--quiet', svn_repo_url, svn_checkout_path])
+
+        # Add some test commits
+        os.chdir(svn_checkout_path)
+
+        write_into_file_at_path("test_file", "test1")
+        cls._svn_add("test_file")
+        cls._svn_commit("initial commit")
+
+        write_into_file_at_path("test_file", "test1test2")
+        # This used to be the last commit, but doing so broke
+        # GitTest.test_apply_git_patch which use the inverse diff of the last commit.
+        # svn-apply fails to remove directories in Git, see:
+        # https://bugs.webkit.org/show_bug.cgi?id=34871
+        os.mkdir("test_dir")
+        # Slash should always be the right path separator since we use cygwin on Windows.
+        test_file3_path = "test_dir/test_file3"
+        write_into_file_at_path(test_file3_path, "third file")
+        cls._svn_add("test_dir")
+        cls._svn_commit("second commit")
+
+        write_into_file_at_path("test_file", "test1test2test3\n")
+        write_into_file_at_path("test_file2", "second file")
+        cls._svn_add("test_file2")
+        cls._svn_commit("third commit")
+
+        # This 4th commit is used to make sure that our patch file handling
+        # code correctly treats patches as binary and does not attempt to
+        # decode them assuming they're utf-8.
+        write_into_file_at_path("test_file", u"latin1 test: \u00A0\n", "latin1")
+        write_into_file_at_path("test_file2", u"utf-8 test: \u00A0\n", "utf-8")
+        cls._svn_commit("fourth commit")
+
+        # svn does not seem to update after commit as I would expect.
+        run_command(['svn', 'update'])
+        remove_dir(svn_checkout_path)
+
+    # This is a hot function since it's invoked by unittest before calling each test_ method in SVNTest and
+    # GitTest. We create a mock SVN repo once and then perform an SVN checkout from a filesystem copy of
+    # it since it's expensive to create the mock repo.
+    @classmethod
+    def setup(cls, test_object):
+        global cached_svn_repo_path
+        if not cached_svn_repo_path:
+            cached_svn_repo_path = cls._setup_mock_repo()
+
+        test_object.temp_directory = tempfile.mkdtemp(suffix="svn_test")
+        test_object.svn_repo_path = os.path.join(test_object.temp_directory, "repo")
+        test_object.svn_repo_url = "file://%s" % test_object.svn_repo_path
+        test_object.svn_checkout_path = os.path.join(test_object.temp_directory, "checkout")
+        shutil.copytree(cached_svn_repo_path, test_object.svn_repo_path)
+        run_command(['svn', 'checkout', '--quiet', test_object.svn_repo_url + "/trunk", test_object.svn_checkout_path])
+
+    @classmethod
+    def _setup_mock_repo(cls):
+        # Create an test SVN repository
+        svn_repo_path = tempfile.mkdtemp(suffix="svn_test_repo")
+        svn_repo_url = "file://%s" % svn_repo_path  # Not sure this will work on windows
+        # git svn complains if we don't pass --pre-1.5-compatible, not sure why:
+        # Expected FS format '2'; found format '3' at /usr/local/libexec/git-core//git-svn line 1477
+        run_command(['svnadmin', 'create', '--pre-1.5-compatible', svn_repo_path])
+
+        # Create a test svn checkout
+        svn_checkout_path = tempfile.mkdtemp(suffix="svn_test_checkout")
+        run_command(['svn', 'checkout', '--quiet', svn_repo_url, svn_checkout_path])
+
+        # Create and checkout a trunk dir to match the standard svn configuration to match git-svn's expectations
+        os.chdir(svn_checkout_path)
+        os.mkdir('trunk')
+        cls._svn_add('trunk')
+        # We can add tags and branches as well if we ever need to test those.
+        cls._svn_commit('add trunk')
+
+        # Change directory out of the svn checkout so we can delete the checkout directory.
+        remove_dir(svn_checkout_path)
+
+        cls._setup_test_commits(svn_repo_url + "/trunk")
+        return svn_repo_path
+
+    @classmethod
+    def tear_down(cls, test_object):
+        remove_dir(test_object.temp_directory)
+
+        # Now that we've deleted the checkout paths, cwddir may be invalid
+        # Change back to a valid directory so that later calls to os.getcwd() do not fail.
+        if os.path.isabs(__file__):
+            path = os.path.dirname(__file__)
+        else:
+            path = sys.path[0]
+        os.chdir(detect_scm_system(path).checkout_root)
+
+
+# For testing the SCM baseclass directly.
+class SCMClassTests(unittest.TestCase):
+    def setUp(self):
+        self.dev_null = open(os.devnull, "w") # Used to make our Popen calls quiet.
+
+    def tearDown(self):
+        self.dev_null.close()
+
+    def test_run_command_with_pipe(self):
+        input_process = subprocess.Popen(['echo', 'foo\nbar'], stdout=subprocess.PIPE, stderr=self.dev_null)
+        self.assertEqual(run_command(['grep', 'bar'], input=input_process.stdout), "bar\n")
+
+        # Test the non-pipe case too:
+        self.assertEqual(run_command(['grep', 'bar'], input="foo\nbar"), "bar\n")
+
+        command_returns_non_zero = ['/bin/sh', '--invalid-option']
+        # Test when the input pipe process fails.
+        input_process = subprocess.Popen(command_returns_non_zero, stdout=subprocess.PIPE, stderr=self.dev_null)
+        self.assertTrue(input_process.poll() != 0)
+        self.assertRaises(ScriptError, run_command, ['grep', 'bar'], input=input_process.stdout)
+
+        # Test when the run_command process fails.
+        input_process = subprocess.Popen(['echo', 'foo\nbar'], stdout=subprocess.PIPE, stderr=self.dev_null) # grep shows usage and calls exit(2) when called w/o arguments.
+        self.assertRaises(ScriptError, run_command, command_returns_non_zero, input=input_process.stdout)
+
+    def test_error_handlers(self):
+        git_failure_message="Merge conflict during commit: Your file or directory 'WebCore/ChangeLog' is probably out-of-date: resource out of date; try updating at /usr/local/libexec/git-core//git-svn line 469"
+        svn_failure_message="""svn: Commit failed (details follow):
+svn: File or directory 'ChangeLog' is out of date; try updating
+svn: resource out of date; try updating
+"""
+        command_does_not_exist = ['does_not_exist', 'invalid_option']
+        self.assertRaises(OSError, run_command, command_does_not_exist)
+        self.assertRaises(OSError, run_command, command_does_not_exist, error_handler=Executive.ignore_error)
+
+        command_returns_non_zero = ['/bin/sh', '--invalid-option']
+        self.assertRaises(ScriptError, run_command, command_returns_non_zero)
+        # Check if returns error text:
+        self.assertTrue(run_command(command_returns_non_zero, error_handler=Executive.ignore_error))
+
+        self.assertRaises(CheckoutNeedsUpdate, commit_error_handler, ScriptError(output=git_failure_message))
+        self.assertRaises(CheckoutNeedsUpdate, commit_error_handler, ScriptError(output=svn_failure_message))
+        self.assertRaises(ScriptError, commit_error_handler, ScriptError(output='blah blah blah'))
+
+
+# GitTest and SVNTest inherit from this so any test_ methods here will be run once for this class and then once for each subclass.
+class SCMTest(unittest.TestCase):
+    def _create_patch(self, patch_contents):
+        # FIXME: This code is brittle if the Attachment API changes.
+        attachment = Attachment({"bug_id": 12345}, None)
+        attachment.contents = lambda: patch_contents
+
+        joe_cool = Committer("Joe Cool", "joe@cool.com")
+        attachment.reviewer = lambda: joe_cool
+
+        return attachment
+
+    def _setup_webkittools_scripts_symlink(self, local_scm):
+        webkit_scm = detect_scm_system(os.path.dirname(os.path.abspath(__file__)))
+        webkit_scripts_directory = webkit_scm.scripts_directory()
+        local_scripts_directory = local_scm.scripts_directory()
+        os.mkdir(os.path.dirname(local_scripts_directory))
+        os.symlink(webkit_scripts_directory, local_scripts_directory)
+
+    # Tests which both GitTest and SVNTest should run.
+    # FIXME: There must be a simpler way to add these w/o adding a wrapper method to both subclasses
+
+    def _shared_test_changed_files(self):
+        write_into_file_at_path("test_file", "changed content")
+        self.assertEqual(self.scm.changed_files(), ["test_file"])
+        write_into_file_at_path("test_dir/test_file3", "new stuff")
+        self.assertEqual(self.scm.changed_files(), ["test_dir/test_file3", "test_file"])
+        old_cwd = os.getcwd()
+        os.chdir("test_dir")
+        # Validate that changed_files does not change with our cwd, see bug 37015.
+        self.assertEqual(self.scm.changed_files(), ["test_dir/test_file3", "test_file"])
+        os.chdir(old_cwd)
+
+    def _shared_test_added_files(self):
+        write_into_file_at_path("test_file", "changed content")
+        self.assertEqual(self.scm.added_files(), [])
+
+        write_into_file_at_path("added_file", "new stuff")
+        self.scm.add("added_file")
+
+        write_into_file_at_path("added_file3", "more new stuff")
+        write_into_file_at_path("added_file4", "more new stuff")
+        self.scm.add_list(["added_file3", "added_file4"])
+
+        os.mkdir("added_dir")
+        write_into_file_at_path("added_dir/added_file2", "new stuff")
+        self.scm.add("added_dir")
+
+        # SVN reports directory changes, Git does not.
+        added_files = self.scm.added_files()
+        if "added_dir" in added_files:
+            added_files.remove("added_dir")
+        self.assertEqual(added_files, ["added_dir/added_file2", "added_file", "added_file3", "added_file4"])
+
+        # Test also to make sure clean_working_directory removes added files
+        self.scm.clean_working_directory()
+        self.assertEqual(self.scm.added_files(), [])
+        self.assertFalse(os.path.exists("added_file"))
+        self.assertFalse(os.path.exists("added_file3"))
+        self.assertFalse(os.path.exists("added_file4"))
+        self.assertFalse(os.path.exists("added_dir"))
+
+    def _shared_test_changed_files_for_revision(self):
+        # SVN reports directory changes, Git does not.
+        changed_files = self.scm.changed_files_for_revision(3)
+        if "test_dir" in changed_files:
+            changed_files.remove("test_dir")
+        self.assertEqual(changed_files, ["test_dir/test_file3", "test_file"])
+        self.assertEqual(sorted(self.scm.changed_files_for_revision(4)), sorted(["test_file", "test_file2"]))  # Git and SVN return different orders.
+        self.assertEqual(self.scm.changed_files_for_revision(2), ["test_file"])
+
+    def _shared_test_contents_at_revision(self):
+        self.assertEqual(self.scm.contents_at_revision("test_file", 3), "test1test2")
+        self.assertEqual(self.scm.contents_at_revision("test_file", 4), "test1test2test3\n")
+
+        # Verify that contents_at_revision returns a byte array, aka str():
+        self.assertEqual(self.scm.contents_at_revision("test_file", 5), u"latin1 test: \u00A0\n".encode("latin1"))
+        self.assertEqual(self.scm.contents_at_revision("test_file2", 5), u"utf-8 test: \u00A0\n".encode("utf-8"))
+
+        self.assertEqual(self.scm.contents_at_revision("test_file2", 4), "second file")
+        # Files which don't exist:
+        # Currently we raise instead of returning None because detecting the difference between
+        # "file not found" and any other error seems impossible with svn (git seems to expose such through the return code).
+        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
+
+    def _shared_test_reverse_diff(self):
+        self._setup_webkittools_scripts_symlink(self.scm) # Git's apply_reverse_diff uses resolve-ChangeLogs
+        # Only test the simple case, as any other will end up with conflict markers.
+        self.scm.apply_reverse_diff('5')
+        self.assertEqual(read_from_path('test_file'), "test1test2test3\n")
+
+    def _shared_test_diff_for_revision(self):
+        # Patch formats are slightly different between svn and git, so just regexp for things we know should be there.
+        r3_patch = self.scm.diff_for_revision(4)
+        self.assertTrue(re.search('test3', r3_patch))
+        self.assertFalse(re.search('test4', r3_patch))
+        self.assertTrue(re.search('test2', r3_patch))
+        self.assertTrue(re.search('test2', self.scm.diff_for_revision(3)))
+
+    def _shared_test_svn_apply_git_patch(self):
+        self._setup_webkittools_scripts_symlink(self.scm)
+        git_binary_addition = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
+new file mode 100644
+index 0000000000000000000000000000000000000000..64a9532e7794fcd791f6f12157406d90
+60151690
+GIT binary patch
+literal 512
+zcmZ?wbhEHbRAx|MU|?iW{Kxc~?KofD;ckY;H+&5HnHl!!GQMD7h+sU{_)e9f^V3c?
+zhJP##HdZC#4K}7F68@!1jfWQg2daCm-gs#3|JREDT>c+pG4L<_2;w##WMO#ysPPap
+zLqpAf1OE938xAsSp4!5f-o><?VKe(#0jEcwfHGF4%M1^kRs14oVBp2ZEL{E1N<-zJ
+zsfLmOtKta;2_;2c#^S1-8cf<nb!QnGl>c!Xe6RXvrEtAWBvSDTgTO1j3vA31Puw!A
+zs(87q)j_mVDTqBo-P+03-P5mHCEnJ+x}YdCuS7#bCCyePUe(ynK+|4b-3qK)T?Z&)
+zYG+`tl4h?GZv_$t82}X4*DTE|$;{DEiPyF@)U-1+FaX++T9H{&%cag`W1|zVP@`%b
+zqiSkp6{BTpWTkCr!=<C6Q=?#~R8^JfrliAF6Q^gV9Iup8RqCXqqhqC`qsyhk<-nlB
+z00f{QZvfK&|Nm#oZ0TQl`Yr$BIa6A@16O26ud7H<QM=xl`toLKnz-3h@9c9q&wm|X
+z{89I|WPyD!*M?gv?q`;L=2YFeXrJQNti4?}s!zFo=5CzeBxC69xA<zrjP<wUcCRh4
+ptUl-ZG<%a~#LwkIWv&q!KSCH7tQ8cJDiw+|GV?MN)RjY50RTb-xvT&H
+
+literal 0
+HcmV?d00001
+
+"""
+        self.checkout.apply_patch(self._create_patch(git_binary_addition))
+        added = read_from_path('fizzbuzz7.gif', encoding=None)
+        self.assertEqual(512, len(added))
+        self.assertTrue(added.startswith('GIF89a'))
+        self.assertTrue('fizzbuzz7.gif' in self.scm.changed_files())
+
+        # The file already exists.
+        self.assertRaises(ScriptError, self.checkout.apply_patch, self._create_patch(git_binary_addition))
+
+        git_binary_modification = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
+index 64a9532e7794fcd791f6f12157406d9060151690..323fae03f4606ea9991df8befbb2fca7
+GIT binary patch
+literal 7
+OcmYex&reD$;sO8*F9L)B
+
+literal 512
+zcmZ?wbhEHbRAx|MU|?iW{Kxc~?KofD;ckY;H+&5HnHl!!GQMD7h+sU{_)e9f^V3c?
+zhJP##HdZC#4K}7F68@!1jfWQg2daCm-gs#3|JREDT>c+pG4L<_2;w##WMO#ysPPap
+zLqpAf1OE938xAsSp4!5f-o><?VKe(#0jEcwfHGF4%M1^kRs14oVBp2ZEL{E1N<-zJ
+zsfLmOtKta;2_;2c#^S1-8cf<nb!QnGl>c!Xe6RXvrEtAWBvSDTgTO1j3vA31Puw!A
+zs(87q)j_mVDTqBo-P+03-P5mHCEnJ+x}YdCuS7#bCCyePUe(ynK+|4b-3qK)T?Z&)
+zYG+`tl4h?GZv_$t82}X4*DTE|$;{DEiPyF@)U-1+FaX++T9H{&%cag`W1|zVP@`%b
+zqiSkp6{BTpWTkCr!=<C6Q=?#~R8^JfrliAF6Q^gV9Iup8RqCXqqhqC`qsyhk<-nlB
+z00f{QZvfK&|Nm#oZ0TQl`Yr$BIa6A@16O26ud7H<QM=xl`toLKnz-3h@9c9q&wm|X
+z{89I|WPyD!*M?gv?q`;L=2YFeXrJQNti4?}s!zFo=5CzeBxC69xA<zrjP<wUcCRh4
+ptUl-ZG<%a~#LwkIWv&q!KSCH7tQ8cJDiw+|GV?MN)RjY50RTb-xvT&H
+
+"""
+        self.checkout.apply_patch(self._create_patch(git_binary_modification))
+        modified = read_from_path('fizzbuzz7.gif', encoding=None)
+        self.assertEqual('foobar\n', modified)
+        self.assertTrue('fizzbuzz7.gif' in self.scm.changed_files())
+
+        # Applying the same modification should fail.
+        self.assertRaises(ScriptError, self.checkout.apply_patch, self._create_patch(git_binary_modification))
+
+        git_binary_deletion = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
+deleted file mode 100644
+index 323fae0..0000000
+GIT binary patch
+literal 0
+HcmV?d00001
+
+literal 7
+OcmYex&reD$;sO8*F9L)B
+
+"""
+        self.checkout.apply_patch(self._create_patch(git_binary_deletion))
+        self.assertFalse(os.path.exists('fizzbuzz7.gif'))
+        self.assertFalse('fizzbuzz7.gif' in self.scm.changed_files())
+
+        # Cannot delete again.
+        self.assertRaises(ScriptError, self.checkout.apply_patch, self._create_patch(git_binary_deletion))
+
+    def _shared_test_add_recursively(self):
+        os.mkdir("added_dir")
+        write_into_file_at_path("added_dir/added_file", "new stuff")
+        self.scm.add("added_dir/added_file")
+        self.assertTrue("added_dir/added_file" in self.scm.added_files())
+
+    def _shared_test_delete_recursively(self):
+        os.mkdir("added_dir")
+        write_into_file_at_path("added_dir/added_file", "new stuff")
+        self.scm.add("added_dir/added_file")
+        self.assertTrue("added_dir/added_file" in self.scm.added_files())
+        self.scm.delete("added_dir/added_file")
+        self.assertFalse("added_dir" in self.scm.added_files())
+
+    def _shared_test_delete_recursively_or_not(self):
+        os.mkdir("added_dir")
+        write_into_file_at_path("added_dir/added_file", "new stuff")
+        write_into_file_at_path("added_dir/another_added_file", "more new stuff")
+        self.scm.add("added_dir/added_file")
+        self.scm.add("added_dir/another_added_file")
+        self.assertTrue("added_dir/added_file" in self.scm.added_files())
+        self.assertTrue("added_dir/another_added_file" in self.scm.added_files())
+        self.scm.delete("added_dir/added_file")
+        self.assertTrue("added_dir/another_added_file" in self.scm.added_files())
+
+    def _shared_test_exists(self, scm, commit_function):
+        os.chdir(scm.checkout_root)
+        self.assertFalse(scm.exists('foo.txt'))
+        write_into_file_at_path('foo.txt', 'some stuff')
+        self.assertFalse(scm.exists('foo.txt'))
+        scm.add('foo.txt')
+        commit_function('adding foo')
+        self.assertTrue(scm.exists('foo.txt'))
+        scm.delete('foo.txt')
+        commit_function('deleting foo')
+        self.assertFalse(scm.exists('foo.txt'))
+
+    def _shared_test_head_svn_revision(self):
+        self.assertEqual(self.scm.head_svn_revision(), '5')
+
+
+# Context manager that overrides the current timezone.
+class TimezoneOverride(object):
+    def __init__(self, timezone_string):
+        self._timezone_string = timezone_string
+
+    def __enter__(self):
+        if hasattr(time, 'tzset'):
+            self._saved_timezone = os.environ.get('TZ', None)
+            os.environ['TZ'] = self._timezone_string
+            time.tzset()
+
+    def __exit__(self, type, value, traceback):
+        if hasattr(time, 'tzset'):
+            if self._saved_timezone:
+                os.environ['TZ'] = self._saved_timezone
+            else:
+                del os.environ['TZ']
+            time.tzset()
+
+
+class SVNTest(SCMTest):
+
+    @staticmethod
+    def _set_date_and_reviewer(changelog_entry):
+        # Joe Cool matches the reviewer set in SCMTest._create_patch
+        changelog_entry = changelog_entry.replace('REVIEWER_HERE', 'Joe Cool')
+        # svn-apply will update ChangeLog entries with today's date (as in Cupertino, CA, US)
+        with TimezoneOverride('PST8PDT'):
+            return changelog_entry.replace('DATE_HERE', date.today().isoformat())
+
+    def test_svn_apply(self):
+        first_entry = """2009-10-26  Eric Seidel  <eric@webkit.org>
+
+        Reviewed by Foo Bar.
+
+        Most awesome change ever.
+
+        * scm_unittest.py:
+"""
+        intermediate_entry = """2009-10-27  Eric Seidel  <eric@webkit.org>
+
+        Reviewed by Baz Bar.
+
+        A more awesomer change yet!
+
+        * scm_unittest.py:
+"""
+        one_line_overlap_patch = """Index: ChangeLog
+===================================================================
+--- ChangeLog	(revision 5)
++++ ChangeLog	(working copy)
+@@ -1,5 +1,13 @@
+ 2009-10-26  Eric Seidel  <eric@webkit.org>
+%(whitespace)s
++        Reviewed by NOBODY (OOPS!).
++
++        Second most awesome change ever.
++
++        * scm_unittest.py:
++
++2009-10-26  Eric Seidel  <eric@webkit.org>
++
+         Reviewed by Foo Bar.
+%(whitespace)s
+         Most awesome change ever.
+""" % {'whitespace': ' '}
+        one_line_overlap_entry = """DATE_HERE  Eric Seidel  <eric@webkit.org>
+
+        Reviewed by REVIEWER_HERE.
+
+        Second most awesome change ever.
+
+        * scm_unittest.py:
+"""
+        two_line_overlap_patch = """Index: ChangeLog
+===================================================================
+--- ChangeLog	(revision 5)
++++ ChangeLog	(working copy)
+@@ -2,6 +2,14 @@
+%(whitespace)s
+         Reviewed by Foo Bar.
+%(whitespace)s
++        Second most awesome change ever.
++
++        * scm_unittest.py:
++
++2009-10-26  Eric Seidel  <eric@webkit.org>
++
++        Reviewed by Foo Bar.
++
+         Most awesome change ever.
+%(whitespace)s
+         * scm_unittest.py:
+""" % {'whitespace': ' '}
+        two_line_overlap_entry = """DATE_HERE  Eric Seidel  <eric@webkit.org>
+
+        Reviewed by Foo Bar.
+
+        Second most awesome change ever.
+
+        * scm_unittest.py:
+"""
+        write_into_file_at_path('ChangeLog', first_entry)
+        run_command(['svn', 'add', 'ChangeLog'])
+        run_command(['svn', 'commit', '--quiet', '--message', 'ChangeLog commit'])
+
+        # Patch files were created against just 'first_entry'.
+        # Add a second commit to make svn-apply have to apply the patches with fuzz.
+        changelog_contents = "%s\n%s" % (intermediate_entry, first_entry)
+        write_into_file_at_path('ChangeLog', changelog_contents)
+        run_command(['svn', 'commit', '--quiet', '--message', 'Intermediate commit'])
+
+        self._setup_webkittools_scripts_symlink(self.scm)
+        self.checkout.apply_patch(self._create_patch(one_line_overlap_patch))
+        expected_changelog_contents = "%s\n%s" % (self._set_date_and_reviewer(one_line_overlap_entry), changelog_contents)
+        self.assertEquals(read_from_path('ChangeLog'), expected_changelog_contents)
+
+        self.scm.revert_files(['ChangeLog'])
+        self.checkout.apply_patch(self._create_patch(two_line_overlap_patch))
+        expected_changelog_contents = "%s\n%s" % (self._set_date_and_reviewer(two_line_overlap_entry), changelog_contents)
+        self.assertEquals(read_from_path('ChangeLog'), expected_changelog_contents)
+
+    def setUp(self):
+        SVNTestRepository.setup(self)
+        os.chdir(self.svn_checkout_path)
+        self.scm = detect_scm_system(self.svn_checkout_path)
+        # For historical reasons, we test some checkout code here too.
+        self.checkout = Checkout(self.scm)
+
+    def tearDown(self):
+        SVNTestRepository.tear_down(self)
+
+    def test_detect_scm_system_relative_url(self):
+        scm = detect_scm_system(".")
+        # I wanted to assert that we got the right path, but there was some
+        # crazy magic with temp folder names that I couldn't figure out.
+        self.assertTrue(scm.checkout_root)
+
+    def test_create_patch_is_full_patch(self):
+        test_dir_path = os.path.join(self.svn_checkout_path, "test_dir2")
+        os.mkdir(test_dir_path)
+        test_file_path = os.path.join(test_dir_path, 'test_file2')
+        write_into_file_at_path(test_file_path, 'test content')
+        run_command(['svn', 'add', 'test_dir2'])
+
+        # create_patch depends on 'svn-create-patch', so make a dummy version.
+        scripts_path = os.path.join(self.svn_checkout_path, 'Tools', 'Scripts')
+        os.makedirs(scripts_path)
+        create_patch_path = os.path.join(scripts_path, 'svn-create-patch')
+        write_into_file_at_path(create_patch_path, '#!/bin/sh\necho $PWD') # We could pass -n to prevent the \n, but not all echo accept -n.
+        os.chmod(create_patch_path, stat.S_IXUSR | stat.S_IRUSR)
+
+        # Change into our test directory and run the create_patch command.
+        os.chdir(test_dir_path)
+        scm = detect_scm_system(test_dir_path)
+        self.assertEqual(scm.checkout_root, self.svn_checkout_path) # Sanity check that detection worked right.
+        patch_contents = scm.create_patch()
+        # Our fake 'svn-create-patch' returns $PWD instead of a patch, check that it was executed from the root of the repo.
+        self.assertEqual("%s\n" % os.path.realpath(scm.checkout_root), patch_contents) # Add a \n because echo adds a \n.
+
+    def test_detection(self):
+        scm = detect_scm_system(self.svn_checkout_path)
+        self.assertEqual(scm.display_name(), "svn")
+        self.assertEqual(scm.supports_local_commits(), False)
+
+    def test_apply_small_binary_patch(self):
+        patch_contents = """Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+Property changes on: test_file.swf
+___________________________________________________________________
+Name: svn:mime-type
+   + application/octet-stream
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+"""
+        expected_contents = base64.b64decode("Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==")
+        self._setup_webkittools_scripts_symlink(self.scm)
+        patch_file = self._create_patch(patch_contents)
+        self.checkout.apply_patch(patch_file)
+        actual_contents = read_from_path("test_file.swf", encoding=None)
+        self.assertEqual(actual_contents, expected_contents)
+
+    def test_apply_svn_patch(self):
+        scm = detect_scm_system(self.svn_checkout_path)
+        patch = self._create_patch(_svn_diff("-r5:4"))
+        self._setup_webkittools_scripts_symlink(scm)
+        Checkout(scm).apply_patch(patch)
+
+    def test_commit_logs(self):
+        # Commits have dates and usernames in them, so we can't just direct compare.
+        self.assertTrue(re.search('fourth commit', self.scm.last_svn_commit_log()))
+        self.assertTrue(re.search('second commit', self.scm.svn_commit_log(3)))
+
+    def _shared_test_commit_with_message(self, username=None):
+        write_into_file_at_path('test_file', 'more test content')
+        commit_text = self.scm.commit_with_message("another test commit", username)
+        self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6')
+
+    def test_commit_in_subdir(self, username=None):
+        write_into_file_at_path('test_dir/test_file3', 'more test content')
+        os.chdir("test_dir")
+        commit_text = self.scm.commit_with_message("another test commit", username)
+        os.chdir("..")
+        self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6')
+
+    def test_commit_text_parsing(self):
+        self._shared_test_commit_with_message()
+
+    def test_commit_with_username(self):
+        self._shared_test_commit_with_message("dbates@webkit.org")
+
+    def test_commit_without_authorization(self):
+        self.scm.has_authorization_for_realm = lambda realm: False
+        self.assertRaises(AuthenticationError, self._shared_test_commit_with_message)
+
+    def test_has_authorization_for_realm_using_credentials_with_passtype(self):
+        credentials = """
+K 8
+passtype
+V 8
+keychain
+K 15
+svn:realmstring
+V 39
+<http://svn.webkit.org:80> Mac OS Forge
+K 8
+username
+V 17
+dbates@webkit.org
+END
+"""
+        self.assertTrue(self._test_has_authorization_for_realm_using_credentials(SVN.svn_server_realm, credentials))
+
+    def test_has_authorization_for_realm_using_credentials_with_password(self):
+        credentials = """
+K 15
+svn:realmstring
+V 39
+<http://svn.webkit.org:80> Mac OS Forge
+K 8
+username
+V 17
+dbates@webkit.org
+K 8
+password
+V 4
+blah
+END
+"""
+        self.assertTrue(self._test_has_authorization_for_realm_using_credentials(SVN.svn_server_realm, credentials))
+
+    def _test_has_authorization_for_realm_using_credentials(self, realm, credentials):
+        scm = detect_scm_system(self.svn_checkout_path)
+        fake_home_dir = tempfile.mkdtemp(suffix="fake_home_dir")
+        svn_config_dir_path = os.path.join(fake_home_dir, ".subversion")
+        os.mkdir(svn_config_dir_path)
+        fake_webkit_auth_file = os.path.join(svn_config_dir_path, "fake_webkit_auth_file")
+        write_into_file_at_path(fake_webkit_auth_file, credentials)
+        result = scm.has_authorization_for_realm(realm, home_directory=fake_home_dir)
+        os.remove(fake_webkit_auth_file)
+        os.rmdir(svn_config_dir_path)
+        os.rmdir(fake_home_dir)
+        return result
+
+    def test_not_have_authorization_for_realm_with_credentials_missing_password_and_passtype(self):
+        credentials = """
+K 15
+svn:realmstring
+V 39
+<http://svn.webkit.org:80> Mac OS Forge
+K 8
+username
+V 17
+dbates@webkit.org
+END
+"""
+        self.assertFalse(self._test_has_authorization_for_realm_using_credentials(SVN.svn_server_realm, credentials))
+
+    def test_not_have_authorization_for_realm_when_missing_credentials_file(self):
+        scm = detect_scm_system(self.svn_checkout_path)
+        fake_home_dir = tempfile.mkdtemp(suffix="fake_home_dir")
+        svn_config_dir_path = os.path.join(fake_home_dir, ".subversion")
+        os.mkdir(svn_config_dir_path)
+        self.assertFalse(scm.has_authorization_for_realm(SVN.svn_server_realm, home_directory=fake_home_dir))
+        os.rmdir(svn_config_dir_path)
+        os.rmdir(fake_home_dir)
+
+    def test_reverse_diff(self):
+        self._shared_test_reverse_diff()
+
+    def test_diff_for_revision(self):
+        self._shared_test_diff_for_revision()
+
+    def test_svn_apply_git_patch(self):
+        self._shared_test_svn_apply_git_patch()
+
+    def test_changed_files(self):
+        self._shared_test_changed_files()
+
+    def test_changed_files_for_revision(self):
+        self._shared_test_changed_files_for_revision()
+
+    def test_added_files(self):
+        self._shared_test_added_files()
+
+    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()
+
+    def test_add_recursively(self):
+        self._shared_test_add_recursively()
+
+    def test_delete(self):
+        os.chdir(self.svn_checkout_path)
+        self.scm.delete("test_file")
+        self.assertTrue("test_file" in self.scm.deleted_files())
+
+    def test_delete_list(self):
+        os.chdir(self.svn_checkout_path)
+        self.scm.delete_list(["test_file", "test_file2"])
+        self.assertTrue("test_file" in self.scm.deleted_files())
+        self.assertTrue("test_file2" in self.scm.deleted_files())
+
+    def test_delete_recursively(self):
+        self._shared_test_delete_recursively()
+
+    def test_delete_recursively_or_not(self):
+        self._shared_test_delete_recursively_or_not()
+
+    def test_head_svn_revision(self):
+        self._shared_test_head_svn_revision()
+
+    def test_propset_propget(self):
+        filepath = os.path.join(self.svn_checkout_path, "test_file")
+        expected_mime_type = "x-application/foo-bar"
+        self.scm.propset("svn:mime-type", expected_mime_type, filepath)
+        self.assertEqual(expected_mime_type, self.scm.propget("svn:mime-type", filepath))
+
+    def test_show_head(self):
+        write_into_file_at_path("test_file", u"Hello!", "utf-8")
+        SVNTestRepository._svn_commit("fourth commit")
+        self.assertEqual("Hello!", self.scm.show_head('test_file'))
+
+    def test_show_head_binary(self):
+        data = "\244"
+        write_into_file_at_path("binary_file", data, encoding=None)
+        self.scm.add("binary_file")
+        self.scm.commit_with_message("a test commit")
+        self.assertEqual(data, self.scm.show_head('binary_file'))
+
+    def do_test_diff_for_file(self):
+        write_into_file_at_path('test_file', 'some content')
+        self.scm.commit_with_message("a test commit")
+        diff = self.scm.diff_for_file('test_file')
+        self.assertEqual(diff, "")
+
+        write_into_file_at_path("test_file", "changed content")
+        diff = self.scm.diff_for_file('test_file')
+        self.assertTrue("-some content" in diff)
+        self.assertTrue("+changed content" in diff)
+
+    def clean_bogus_dir(self):
+        self.bogus_dir = self.scm._bogus_dir_name()
+        if os.path.exists(self.bogus_dir):
+            shutil.rmtree(self.bogus_dir)
+
+    def test_diff_for_file_with_existing_bogus_dir(self):
+        self.clean_bogus_dir()
+        os.mkdir(self.bogus_dir)
+        self.do_test_diff_for_file()
+        self.assertTrue(os.path.exists(self.bogus_dir))
+        shutil.rmtree(self.bogus_dir)
+
+    def test_diff_for_file_with_missing_bogus_dir(self):
+        self.clean_bogus_dir()
+        self.do_test_diff_for_file()
+        self.assertFalse(os.path.exists(self.bogus_dir))
+
+    def test_svn_lock(self):
+        svn_root_lock_path = ".svn/lock"
+        write_into_file_at_path(svn_root_lock_path, "", "utf-8")
+        # webkit-patch uses a Checkout object and runs update-webkit, just use svn update here.
+        self.assertRaises(ScriptError, run_command, ['svn', 'update'])
+        self.scm.clean_working_directory()
+        self.assertFalse(os.path.exists(svn_root_lock_path))
+        run_command(['svn', 'update'])  # Should succeed and not raise.
+
+    def test_exists(self):
+        self._shared_test_exists(self.scm, self.scm.commit_with_message)
+
+class GitTest(SCMTest):
+
+    def setUp(self):
+        """Sets up fresh git repository with one commit. Then setups a second git
+        repo that tracks the first one."""
+        # FIXME: We should instead clone a git repo that is tracking an SVN repo.
+        # That better matches what we do with WebKit.
+        self.original_dir = os.getcwd()
+
+        self.untracking_checkout_path = tempfile.mkdtemp(suffix="git_test_checkout2")
+        run_command(['git', 'init', self.untracking_checkout_path])
+
+        os.chdir(self.untracking_checkout_path)
+        write_into_file_at_path('foo_file', 'foo')
+        run_command(['git', 'add', 'foo_file'])
+        run_command(['git', 'commit', '-am', 'dummy commit'])
+        self.untracking_scm = detect_scm_system(self.untracking_checkout_path)
+
+        self.tracking_git_checkout_path = tempfile.mkdtemp(suffix="git_test_checkout")
+        run_command(['git', 'clone', '--quiet', self.untracking_checkout_path, self.tracking_git_checkout_path])
+        os.chdir(self.tracking_git_checkout_path)
+        self.tracking_scm = detect_scm_system(self.tracking_git_checkout_path)
+
+    def tearDown(self):
+        # Change back to a valid directory so that later calls to os.getcwd() do not fail.
+        os.chdir(self.original_dir)
+        run_command(['rm', '-rf', self.tracking_git_checkout_path])
+        run_command(['rm', '-rf', self.untracking_checkout_path])
+
+    def test_remote_branch_ref(self):
+        self.assertEqual(self.tracking_scm.remote_branch_ref(), 'refs/remotes/origin/master')
+
+        os.chdir(self.untracking_checkout_path)
+        self.assertRaises(ScriptError, self.untracking_scm.remote_branch_ref)
+
+    def test_multiple_remotes(self):
+        run_command(['git', 'config', '--add', 'svn-remote.svn.fetch', 'trunk:remote1'])
+        run_command(['git', 'config', '--add', 'svn-remote.svn.fetch', 'trunk:remote2'])
+        self.assertEqual(self.tracking_scm.remote_branch_ref(), 'remote1')
+
+    def test_create_patch(self):
+        write_into_file_at_path('test_file_commit1', 'contents')
+        run_command(['git', 'add', 'test_file_commit1'])
+        scm = self.tracking_scm
+        scm.commit_locally_with_message('message')
+
+        patch = scm.create_patch()
+        self.assertFalse(re.search(r'Subversion Revision:', patch))
+
+    def test_orderfile(self):
+        os.mkdir("Tools")
+        os.mkdir("Source")
+        os.mkdir("LayoutTests")
+        os.mkdir("Websites")
+
+        # Slash should always be the right path separator since we use cygwin on Windows.
+        Tools_ChangeLog = "Tools/ChangeLog"
+        write_into_file_at_path(Tools_ChangeLog, "contents")
+        Source_ChangeLog = "Source/ChangeLog"
+        write_into_file_at_path(Source_ChangeLog, "contents")
+        LayoutTests_ChangeLog = "LayoutTests/ChangeLog"
+        write_into_file_at_path(LayoutTests_ChangeLog, "contents")
+        Websites_ChangeLog = "Websites/ChangeLog"
+        write_into_file_at_path(Websites_ChangeLog, "contents")
+
+        Tools_ChangeFile = "Tools/ChangeFile"
+        write_into_file_at_path(Tools_ChangeFile, "contents")
+        Source_ChangeFile = "Source/ChangeFile"
+        write_into_file_at_path(Source_ChangeFile, "contents")
+        LayoutTests_ChangeFile = "LayoutTests/ChangeFile"
+        write_into_file_at_path(LayoutTests_ChangeFile, "contents")
+        Websites_ChangeFile = "Websites/ChangeFile"
+        write_into_file_at_path(Websites_ChangeFile, "contents")
+
+        run_command(['git', 'add', 'Tools/ChangeLog'])
+        run_command(['git', 'add', 'LayoutTests/ChangeLog'])
+        run_command(['git', 'add', 'Source/ChangeLog'])
+        run_command(['git', 'add', 'Websites/ChangeLog'])
+        run_command(['git', 'add', 'Tools/ChangeFile'])
+        run_command(['git', 'add', 'LayoutTests/ChangeFile'])
+        run_command(['git', 'add', 'Source/ChangeFile'])
+        run_command(['git', 'add', 'Websites/ChangeFile'])
+        scm = self.tracking_scm
+        scm.commit_locally_with_message('message')
+
+        patch = scm.create_patch()
+        self.assertTrue(re.search(r'Tools/ChangeLog', patch).start() < re.search(r'Tools/ChangeFile', patch).start())
+        self.assertTrue(re.search(r'Websites/ChangeLog', patch).start() < re.search(r'Websites/ChangeFile', patch).start())
+        self.assertTrue(re.search(r'Source/ChangeLog', patch).start() < re.search(r'Source/ChangeFile', patch).start())
+        self.assertTrue(re.search(r'LayoutTests/ChangeLog', patch).start() < re.search(r'LayoutTests/ChangeFile', patch).start())
+
+        self.assertTrue(re.search(r'Source/ChangeLog', patch).start() < re.search(r'LayoutTests/ChangeLog', patch).start())
+        self.assertTrue(re.search(r'Tools/ChangeLog', patch).start() < re.search(r'LayoutTests/ChangeLog', patch).start())
+        self.assertTrue(re.search(r'Websites/ChangeLog', patch).start() < re.search(r'LayoutTests/ChangeLog', patch).start())
+
+        self.assertTrue(re.search(r'Source/ChangeFile', patch).start() < re.search(r'LayoutTests/ChangeLog', patch).start())
+        self.assertTrue(re.search(r'Tools/ChangeFile', patch).start() < re.search(r'LayoutTests/ChangeLog', patch).start())
+        self.assertTrue(re.search(r'Websites/ChangeFile', patch).start() < re.search(r'LayoutTests/ChangeLog', patch).start())
+
+        self.assertTrue(re.search(r'Source/ChangeFile', patch).start() < re.search(r'LayoutTests/ChangeFile', patch).start())
+        self.assertTrue(re.search(r'Tools/ChangeFile', patch).start() < re.search(r'LayoutTests/ChangeFile', patch).start())
+        self.assertTrue(re.search(r'Websites/ChangeFile', patch).start() < re.search(r'LayoutTests/ChangeFile', patch).start())
+
+    def test_exists(self):
+        scm = self.untracking_scm
+        self._shared_test_exists(scm, scm.commit_locally_with_message)
+
+    def test_head_svn_revision(self):
+        scm = detect_scm_system(self.untracking_checkout_path)
+        # If we cloned a git repo tracking an SVG repo, this would give the same result as
+        # self._shared_test_head_svn_revision().
+        self.assertEqual(scm.head_svn_revision(), '')
+
+    def test_rename_files(self):
+        scm = self.tracking_scm
+
+        run_command(['git', 'mv', 'foo_file', 'bar_file'])
+        scm.commit_locally_with_message('message')
+
+        patch = scm.create_patch()
+        self.assertFalse(re.search(r'rename from ', patch))
+        self.assertFalse(re.search(r'rename to ', patch))
+
+
+class GitSVNTest(SCMTest):
+
+    def _setup_git_checkout(self):
+        self.git_checkout_path = tempfile.mkdtemp(suffix="git_test_checkout")
+        # --quiet doesn't make git svn silent, so we use run_silent to redirect output
+        run_silent(['git', 'svn', 'clone', '-T', 'trunk', self.svn_repo_url, self.git_checkout_path])
+        os.chdir(self.git_checkout_path)
+
+    def _tear_down_git_checkout(self):
+        # Change back to a valid directory so that later calls to os.getcwd() do not fail.
+        os.chdir(self.original_dir)
+        run_command(['rm', '-rf', self.git_checkout_path])
+
+    def setUp(self):
+        self.original_dir = os.getcwd()
+
+        SVNTestRepository.setup(self)
+        self._setup_git_checkout()
+        self.scm = detect_scm_system(self.git_checkout_path)
+        # For historical reasons, we test some checkout code here too.
+        self.checkout = Checkout(self.scm)
+
+    def tearDown(self):
+        SVNTestRepository.tear_down(self)
+        self._tear_down_git_checkout()
+
+    def test_detection(self):
+        scm = detect_scm_system(self.git_checkout_path)
+        self.assertEqual(scm.display_name(), "git")
+        self.assertEqual(scm.supports_local_commits(), True)
+
+    def test_read_git_config(self):
+        key = 'test.git-config'
+        value = 'git-config value'
+        run_command(['git', 'config', key, value])
+        self.assertEqual(self.scm.read_git_config(key), value)
+
+    def test_local_commits(self):
+        test_file = os.path.join(self.git_checkout_path, 'test_file')
+        write_into_file_at_path(test_file, 'foo')
+        run_command(['git', 'commit', '-a', '-m', 'local commit'])
+
+        self.assertEqual(len(self.scm.local_commits()), 1)
+
+    def test_discard_local_commits(self):
+        test_file = os.path.join(self.git_checkout_path, 'test_file')
+        write_into_file_at_path(test_file, 'foo')
+        run_command(['git', 'commit', '-a', '-m', 'local commit'])
+
+        self.assertEqual(len(self.scm.local_commits()), 1)
+        self.scm.discard_local_commits()
+        self.assertEqual(len(self.scm.local_commits()), 0)
+
+    def test_delete_branch(self):
+        new_branch = 'foo'
+
+        run_command(['git', 'checkout', '-b', new_branch])
+        self.assertEqual(run_command(['git', 'symbolic-ref', 'HEAD']).strip(), 'refs/heads/' + new_branch)
+
+        run_command(['git', 'checkout', '-b', 'bar'])
+        self.scm.delete_branch(new_branch)
+
+        self.assertFalse(re.search(r'foo', run_command(['git', 'branch'])))
+
+    def test_remote_merge_base(self):
+        # Diff to merge-base should include working-copy changes,
+        # which the diff to svn_branch.. doesn't.
+        test_file = os.path.join(self.git_checkout_path, 'test_file')
+        write_into_file_at_path(test_file, 'foo')
+
+        diff_to_common_base = _git_diff(self.scm.remote_branch_ref() + '..')
+        diff_to_merge_base = _git_diff(self.scm.remote_merge_base())
+
+        self.assertFalse(re.search(r'foo', diff_to_common_base))
+        self.assertTrue(re.search(r'foo', diff_to_merge_base))
+
+    def test_rebase_in_progress(self):
+        svn_test_file = os.path.join(self.svn_checkout_path, 'test_file')
+        write_into_file_at_path(svn_test_file, "svn_checkout")
+        run_command(['svn', 'commit', '--message', 'commit to conflict with git commit'], cwd=self.svn_checkout_path)
+
+        git_test_file = os.path.join(self.git_checkout_path, 'test_file')
+        write_into_file_at_path(git_test_file, "git_checkout")
+        run_command(['git', 'commit', '-a', '-m', 'commit to be thrown away by rebase abort'])
+
+        # --quiet doesn't make git svn silent, so use run_silent to redirect output
+        self.assertRaises(ScriptError, run_silent, ['git', 'svn', '--quiet', 'rebase']) # Will fail due to a conflict leaving us mid-rebase.
+
+        scm = detect_scm_system(self.git_checkout_path)
+        self.assertTrue(scm.rebase_in_progress())
+
+        # Make sure our cleanup works.
+        scm.clean_working_directory()
+        self.assertFalse(scm.rebase_in_progress())
+
+        # Make sure cleanup doesn't throw when no rebase is in progress.
+        scm.clean_working_directory()
+
+    def test_commitish_parsing(self):
+        scm = detect_scm_system(self.git_checkout_path)
+
+        # Multiple revisions are cherry-picked.
+        self.assertEqual(len(scm.commit_ids_from_commitish_arguments(['HEAD~2'])), 1)
+        self.assertEqual(len(scm.commit_ids_from_commitish_arguments(['HEAD', 'HEAD~2'])), 2)
+
+        # ... is an invalid range specifier
+        self.assertRaises(ScriptError, scm.commit_ids_from_commitish_arguments, ['trunk...HEAD'])
+
+    def test_commitish_order(self):
+        scm = detect_scm_system(self.git_checkout_path)
+
+        commit_range = 'HEAD~3..HEAD'
+
+        actual_commits = scm.commit_ids_from_commitish_arguments([commit_range])
+        expected_commits = []
+        expected_commits += reversed(run_command(['git', 'rev-list', commit_range]).splitlines())
+
+        self.assertEqual(actual_commits, expected_commits)
+
+    def test_apply_git_patch(self):
+        scm = detect_scm_system(self.git_checkout_path)
+        # We carefullly pick a diff which does not have a directory addition
+        # as currently svn-apply will error out when trying to remove directories
+        # in Git: https://bugs.webkit.org/show_bug.cgi?id=34871
+        patch = self._create_patch(_git_diff('HEAD..HEAD^'))
+        self._setup_webkittools_scripts_symlink(scm)
+        Checkout(scm).apply_patch(patch)
+
+    def test_commit_text_parsing(self):
+        write_into_file_at_path('test_file', 'more test content')
+        commit_text = self.scm.commit_with_message("another test commit")
+        self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '6')
+
+    def test_commit_with_message_working_copy_only(self):
+        write_into_file_at_path('test_file_commit1', 'more test content')
+        run_command(['git', 'add', 'test_file_commit1'])
+        scm = detect_scm_system(self.git_checkout_path)
+        commit_text = scm.commit_with_message("yet another test commit")
+
+        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
+        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
+        self.assertTrue(re.search(r'test_file_commit1', svn_log))
+
+    def _local_commit(self, filename, contents, message):
+        write_into_file_at_path(filename, contents)
+        run_command(['git', 'add', filename])
+        self.scm.commit_locally_with_message(message)
+
+    def _one_local_commit(self):
+        self._local_commit('test_file_commit1', 'more test content', 'another test commit')
+
+    def _one_local_commit_plus_working_copy_changes(self):
+        self._one_local_commit()
+        write_into_file_at_path('test_file_commit2', 'still more test content')
+        run_command(['git', 'add', 'test_file_commit2'])
+
+    def _second_local_commit(self):
+        self._local_commit('test_file_commit2', 'still more test content', 'yet another test commit')
+
+    def _two_local_commits(self):
+        self._one_local_commit()
+        self._second_local_commit()
+
+    def _three_local_commits(self):
+        self._local_commit('test_file_commit0', 'more test content', '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)
+        self.assertRaises(AmbiguousCommitError, scm.commit_with_message, "yet another test commit")
+        commit_text = scm.commit_with_message("yet another test commit", force_squash=True)
+
+        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
+        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
+        self.assertTrue(re.search(r'test_file_commit2', svn_log))
+        self.assertTrue(re.search(r'test_file_commit1', svn_log))
+
+    def test_commit_with_message_git_commit(self):
+        self._two_local_commits()
+
+        scm = detect_scm_system(self.git_checkout_path)
+        commit_text = scm.commit_with_message("another test commit", git_commit="HEAD^")
+        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
+
+        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
+        self.assertTrue(re.search(r'test_file_commit1', svn_log))
+        self.assertFalse(re.search(r'test_file_commit2', svn_log))
+
+    def test_commit_with_message_git_commit_range(self):
+        self._three_local_commits()
+
+        scm = detect_scm_system(self.git_checkout_path)
+        commit_text = scm.commit_with_message("another test commit", git_commit="HEAD~2..HEAD")
+        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
+
+        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
+        self.assertFalse(re.search(r'test_file_commit0', svn_log))
+        self.assertTrue(re.search(r'test_file_commit1', svn_log))
+        self.assertTrue(re.search(r'test_file_commit2', svn_log))
+
+    def test_commit_with_message_only_local_commit(self):
+        self._one_local_commit()
+        scm = detect_scm_system(self.git_checkout_path)
+        commit_text = scm.commit_with_message("another test commit")
+        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
+        self.assertTrue(re.search(r'test_file_commit1', svn_log))
+
+    def test_commit_with_message_multiple_local_commits_and_working_copy(self):
+        self._two_local_commits()
+        write_into_file_at_path('test_file_commit1', 'working copy change')
+        scm = detect_scm_system(self.git_checkout_path)
+
+        self.assertRaises(AmbiguousCommitError, scm.commit_with_message, "another test commit")
+        commit_text = scm.commit_with_message("another test commit", force_squash=True)
+
+        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
+        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
+        self.assertTrue(re.search(r'test_file_commit2', svn_log))
+        self.assertTrue(re.search(r'test_file_commit1', svn_log))
+
+    def test_commit_with_message_git_commit_and_working_copy(self):
+        self._two_local_commits()
+        write_into_file_at_path('test_file_commit1', 'working copy change')
+        scm = detect_scm_system(self.git_checkout_path)
+        self.assertRaises(ScriptError, scm.commit_with_message, "another test commit", git_commit="HEAD^")
+
+    def test_commit_with_message_multiple_local_commits_always_squash(self):
+        self._two_local_commits()
+        scm = detect_scm_system(self.git_checkout_path)
+        scm._assert_can_squash = lambda working_directory_is_clean: True
+        commit_text = scm.commit_with_message("yet another test commit")
+        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
+
+        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
+        self.assertTrue(re.search(r'test_file_commit2', svn_log))
+        self.assertTrue(re.search(r'test_file_commit1', svn_log))
+
+    def test_commit_with_message_multiple_local_commits(self):
+        self._two_local_commits()
+        scm = detect_scm_system(self.git_checkout_path)
+        self.assertRaises(AmbiguousCommitError, scm.commit_with_message, "yet another test commit")
+        commit_text = scm.commit_with_message("yet another test commit", force_squash=True)
+
+        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
+
+        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
+        self.assertTrue(re.search(r'test_file_commit2', svn_log))
+        self.assertTrue(re.search(r'test_file_commit1', svn_log))
+
+    def test_commit_with_message_not_synced(self):
+        run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
+        self._two_local_commits()
+        scm = detect_scm_system(self.git_checkout_path)
+        self.assertRaises(AmbiguousCommitError, scm.commit_with_message, "another test commit")
+        commit_text = scm.commit_with_message("another test commit", force_squash=True)
+
+        self.assertEqual(scm.svn_revision_from_commit_text(commit_text), '6')
+
+        svn_log = run_command(['git', 'svn', 'log', '--limit=1', '--verbose'])
+        self.assertFalse(re.search(r'test_file2', svn_log))
+        self.assertTrue(re.search(r'test_file_commit2', svn_log))
+        self.assertTrue(re.search(r'test_file_commit1', svn_log))
+
+    def test_commit_with_message_not_synced_with_conflict(self):
+        run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
+        self._local_commit('test_file2', 'asdf', 'asdf commit')
+
+        scm = detect_scm_system(self.git_checkout_path)
+        # There's a conflict between trunk and the test_file2 modification.
+        self.assertRaises(ScriptError, scm.commit_with_message, "another test commit", force_squash=True)
+
+    def test_upstream_branch(self):
+        run_command(['git', 'checkout', '-t', '-b', 'my-branch'])
+        run_command(['git', 'checkout', '-t', '-b', 'my-second-branch'])
+        self.assertEquals(self.scm._upstream_branch(), 'my-branch')
+
+    def test_remote_branch_ref(self):
+        self.assertEqual(self.scm.remote_branch_ref(), 'refs/remotes/trunk')
+
+    def test_reverse_diff(self):
+        self._shared_test_reverse_diff()
+
+    def test_diff_for_revision(self):
+        self._shared_test_diff_for_revision()
+
+    def test_svn_apply_git_patch(self):
+        self._shared_test_svn_apply_git_patch()
+
+    def test_create_patch_local_plus_working_copy(self):
+        self._one_local_commit_plus_working_copy_changes()
+        scm = detect_scm_system(self.git_checkout_path)
+        patch = scm.create_patch()
+        self.assertTrue(re.search(r'test_file_commit1', patch))
+        self.assertTrue(re.search(r'test_file_commit2', patch))
+
+    def test_create_patch(self):
+        self._one_local_commit_plus_working_copy_changes()
+        scm = detect_scm_system(self.git_checkout_path)
+        patch = scm.create_patch()
+        self.assertTrue(re.search(r'test_file_commit2', patch))
+        self.assertTrue(re.search(r'test_file_commit1', patch))
+        self.assertTrue(re.search(r'Subversion Revision: 5', patch))
+
+    def test_create_patch_after_merge(self):
+        run_command(['git', 'checkout', '-b', 'dummy-branch', 'trunk~3'])
+        self._one_local_commit()
+        run_command(['git', 'merge', 'trunk'])
+
+        scm = detect_scm_system(self.git_checkout_path)
+        patch = scm.create_patch()
+        self.assertTrue(re.search(r'test_file_commit1', patch))
+        self.assertTrue(re.search(r'Subversion Revision: 5', 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)
+        patch = scm.create_patch(git_commit="HEAD^")
+        self.assertTrue(re.search(r'test_file_commit1', patch))
+        self.assertFalse(re.search(r'test_file_commit2', patch))
+
+    def test_create_patch_git_commit_range(self):
+        self._three_local_commits()
+        scm = detect_scm_system(self.git_checkout_path)
+        patch = scm.create_patch(git_commit="HEAD~2..HEAD")
+        self.assertFalse(re.search(r'test_file_commit0', patch))
+        self.assertTrue(re.search(r'test_file_commit2', patch))
+        self.assertTrue(re.search(r'test_file_commit1', patch))
+
+    def test_create_patch_working_copy_only(self):
+        self._one_local_commit_plus_working_copy_changes()
+        scm = detect_scm_system(self.git_checkout_path)
+        patch = scm.create_patch(git_commit="HEAD....")
+        self.assertFalse(re.search(r'test_file_commit1', patch))
+        self.assertTrue(re.search(r'test_file_commit2', patch))
+
+    def test_create_patch_multiple_local_commits(self):
+        self._two_local_commits()
+        scm = detect_scm_system(self.git_checkout_path)
+        patch = scm.create_patch()
+        self.assertTrue(re.search(r'test_file_commit2', patch))
+        self.assertTrue(re.search(r'test_file_commit1', patch))
+
+    def test_create_patch_not_synced(self):
+        run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
+        self._two_local_commits()
+        scm = detect_scm_system(self.git_checkout_path)
+        patch = scm.create_patch()
+        self.assertFalse(re.search(r'test_file2', patch))
+        self.assertTrue(re.search(r'test_file_commit2', patch))
+        self.assertTrue(re.search(r'test_file_commit1', patch))
+
+    def test_create_binary_patch(self):
+        # Create a git binary patch and check the contents.
+        scm = detect_scm_system(self.git_checkout_path)
+        test_file_name = 'binary_file'
+        test_file_path = os.path.join(self.git_checkout_path, test_file_name)
+        file_contents = ''.join(map(chr, range(256)))
+        write_into_file_at_path(test_file_path, file_contents, encoding=None)
+        run_command(['git', 'add', test_file_name])
+        patch = scm.create_patch()
+        self.assertTrue(re.search(r'\nliteral 0\n', patch))
+        self.assertTrue(re.search(r'\nliteral 256\n', patch))
+
+        # Check if we can apply the created patch.
+        run_command(['git', 'rm', '-f', test_file_name])
+        self._setup_webkittools_scripts_symlink(scm)
+        self.checkout.apply_patch(self._create_patch(patch))
+        self.assertEqual(file_contents, read_from_path(test_file_path, encoding=None))
+
+        # Check if we can create a patch from a local commit.
+        write_into_file_at_path(test_file_path, file_contents, encoding=None)
+        run_command(['git', 'add', test_file_name])
+        run_command(['git', 'commit', '-m', 'binary diff'])
+        patch_from_local_commit = scm.create_patch('HEAD')
+        self.assertTrue(re.search(r'\nliteral 0\n', patch_from_local_commit))
+        self.assertTrue(re.search(r'\nliteral 256\n', patch_from_local_commit))
+
+    def test_changed_files_local_plus_working_copy(self):
+        self._one_local_commit_plus_working_copy_changes()
+        scm = detect_scm_system(self.git_checkout_path)
+        files = scm.changed_files()
+        self.assertTrue('test_file_commit1' in files)
+        self.assertTrue('test_file_commit2' in files)
+
+        # working copy should *not* be in the list.
+        files = scm.changed_files('trunk..')
+        self.assertTrue('test_file_commit1' in files)
+        self.assertFalse('test_file_commit2' in files)
+
+        # working copy *should* be in the list.
+        files = scm.changed_files('trunk....')
+        self.assertTrue('test_file_commit1' in files)
+        self.assertTrue('test_file_commit2' in files)
+
+    def test_changed_files_git_commit(self):
+        self._two_local_commits()
+        scm = detect_scm_system(self.git_checkout_path)
+        files = scm.changed_files(git_commit="HEAD^")
+        self.assertTrue('test_file_commit1' in files)
+        self.assertFalse('test_file_commit2' in files)
+
+    def test_changed_files_git_commit_range(self):
+        self._three_local_commits()
+        scm = detect_scm_system(self.git_checkout_path)
+        files = scm.changed_files(git_commit="HEAD~2..HEAD")
+        self.assertTrue('test_file_commit0' not in files)
+        self.assertTrue('test_file_commit1' in files)
+        self.assertTrue('test_file_commit2' in files)
+
+    def test_changed_files_working_copy_only(self):
+        self._one_local_commit_plus_working_copy_changes()
+        scm = detect_scm_system(self.git_checkout_path)
+        files = scm.changed_files(git_commit="HEAD....")
+        self.assertFalse('test_file_commit1' in files)
+        self.assertTrue('test_file_commit2' in files)
+
+    def test_changed_files_multiple_local_commits(self):
+        self._two_local_commits()
+        scm = detect_scm_system(self.git_checkout_path)
+        files = scm.changed_files()
+        self.assertTrue('test_file_commit2' in files)
+        self.assertTrue('test_file_commit1' in files)
+
+    def test_changed_files_not_synced(self):
+        run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
+        self._two_local_commits()
+        scm = detect_scm_system(self.git_checkout_path)
+        files = scm.changed_files()
+        self.assertFalse('test_file2' in files)
+        self.assertTrue('test_file_commit2' in files)
+        self.assertTrue('test_file_commit1' in files)
+
+    def test_changed_files_not_synced(self):
+        run_command(['git', 'checkout', '-b', 'my-branch', 'trunk~3'])
+        self._two_local_commits()
+        scm = detect_scm_system(self.git_checkout_path)
+        files = scm.changed_files()
+        self.assertFalse('test_file2' in files)
+        self.assertTrue('test_file_commit2' in files)
+        self.assertTrue('test_file_commit1' in files)
+
+    def test_changed_files(self):
+        self._shared_test_changed_files()
+
+    def test_changed_files_for_revision(self):
+        self._shared_test_changed_files_for_revision()
+
+    def test_changed_files_upstream(self):
+        run_command(['git', 'checkout', '-t', '-b', 'my-branch'])
+        self._one_local_commit()
+        run_command(['git', 'checkout', '-t', '-b', 'my-second-branch'])
+        self._second_local_commit()
+        write_into_file_at_path('test_file_commit0', 'more test content')
+        run_command(['git', 'add', 'test_file_commit0'])
+
+        # equivalent to 'git diff my-branch..HEAD, should not include working changes
+        files = self.scm.changed_files(git_commit='UPSTREAM..')
+        self.assertFalse('test_file_commit1' in files)
+        self.assertTrue('test_file_commit2' in files)
+        self.assertFalse('test_file_commit0' in files)
+
+        # equivalent to 'git diff my-branch', *should* include working changes
+        files = self.scm.changed_files(git_commit='UPSTREAM....')
+        self.assertFalse('test_file_commit1' in files)
+        self.assertTrue('test_file_commit2' in files)
+        self.assertTrue('test_file_commit0' in files)
+
+    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()
+
+    def test_committer_email_for_revision(self):
+        self._shared_test_committer_email_for_revision()
+
+    def test_add_recursively(self):
+        self._shared_test_add_recursively()
+
+    def test_delete(self):
+        self._two_local_commits()
+        self.scm.delete('test_file_commit1')
+        self.assertTrue("test_file_commit1" in self.scm.deleted_files())
+
+    def test_delete_list(self):
+        self._two_local_commits()
+        self.scm.delete_list(["test_file_commit1", "test_file_commit2"])
+        self.assertTrue("test_file_commit1" in self.scm.deleted_files())
+        self.assertTrue("test_file_commit2" in self.scm.deleted_files())
+
+    def test_delete_recursively(self):
+        self._shared_test_delete_recursively()
+
+    def test_delete_recursively_or_not(self):
+        self._shared_test_delete_recursively_or_not()
+
+    def test_head_svn_revision(self):
+        self._shared_test_head_svn_revision()
+
+    def test_to_object_name(self):
+        relpath = 'test_file_commit1'
+        fullpath = os.path.join(self.git_checkout_path, relpath)
+        self._two_local_commits()
+        self.assertEqual(relpath, self.scm.to_object_name(fullpath))
+
+    def test_show_head(self):
+        self._two_local_commits()
+        self.assertEqual("more test content", self.scm.show_head('test_file_commit1'))
+
+    def test_show_head_binary(self):
+        self._two_local_commits()
+        data = "\244"
+        write_into_file_at_path("binary_file", data, encoding=None)
+        self.scm.add("binary_file")
+        self.scm.commit_locally_with_message("a test commit")
+        self.assertEqual(data, self.scm.show_head('binary_file'))
+
+    def test_diff_for_file(self):
+        self._two_local_commits()
+        write_into_file_at_path('test_file_commit1', "Updated", encoding=None)
+
+        diff = self.scm.diff_for_file('test_file_commit1')
+        cached_diff = self.scm.diff_for_file('test_file_commit1')
+        self.assertTrue("+Updated" in diff)
+        self.assertTrue("-more test content" in diff)
+
+        self.scm.add('test_file_commit1')
+
+        cached_diff = self.scm.diff_for_file('test_file_commit1')
+        self.assertTrue("+Updated" in cached_diff)
+        self.assertTrue("-more test content" in cached_diff)
+
+    def test_exists(self):
+        scm = detect_scm_system(self.git_checkout_path)
+        self._shared_test_exists(scm, scm.commit_locally_with_message)
+
+
+# We need to split off more of these SCM tests to use mocks instead of the filesystem.
+# This class is the first part of that.
+class GitTestWithMock(unittest.TestCase):
+    def make_scm(self, logging_executive=False):
+        # We do this should_log dance to avoid logging when Git.__init__ runs sysctl on mac to check for 64-bit support.
+        scm = Git(cwd=None, executive=MockExecutive())
+        scm._executive._should_log = logging_executive
+        return scm
+
+    def test_create_patch(self):
+        scm = self.make_scm(logging_executive=True)
+        expected_stderr = "MOCK run_command: ['git', 'merge-base', u'refs/remotes/origin/master', 'HEAD'], cwd=%(checkout)s\nMOCK run_command: ['git', 'diff', '--binary', '--no-ext-diff', '--full-index', '-M', 'MOCK output of child process', '--'], cwd=%(checkout)s\nMOCK run_command: ['git', 'log', '-25'], cwd=None\n" % {'checkout': scm.checkout_root}
+        OutputCapture().assert_outputs(self, scm.create_patch, expected_stderr=expected_stderr)
+
+    def test_push_local_commits_to_server_with_username_and_password(self):
+        self.assertEquals(self.make_scm().push_local_commits_to_server(username='dbates@webkit.org', password='blah'), "MOCK output of child process")
+
+    def test_push_local_commits_to_server_without_username_and_password(self):
+        self.assertRaises(AuthenticationError, self.make_scm().push_local_commits_to_server)
+
+    def test_push_local_commits_to_server_with_username_and_without_password(self):
+        self.assertRaises(AuthenticationError, self.make_scm().push_local_commits_to_server, {'username': 'dbates@webkit.org'})
+
+    def test_push_local_commits_to_server_without_username_and_with_password(self):
+        self.assertRaises(AuthenticationError, self.make_scm().push_local_commits_to_server, {'password': 'blah'})
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/svn.py b/Tools/Scripts/webkitpy/common/checkout/scm/svn.py
new file mode 100644
index 0000000..25b7e3b
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checkout/scm/svn.py
@@ -0,0 +1,356 @@
+# Copyright (c) 2009, 2010, 2011 Google Inc. All rights reserved.
+# Copyright (c) 2009 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:
+#
+#     * 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 logging
+import os
+import re
+import shutil
+import sys
+
+from webkitpy.common.memoized import memoized
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.common.system.executive import Executive, ScriptError
+
+from .scm import AuthenticationError, SCM, commit_error_handler
+
+
+_log = logging.getLogger(__name__)
+
+
+# A mixin class that represents common functionality for SVN and Git-SVN.
+class SVNRepository:
+    def has_authorization_for_realm(self, realm, home_directory=os.getenv("HOME")):
+        # Assumes find and grep are installed.
+        if not os.path.isdir(os.path.join(home_directory, ".subversion")):
+            return False
+        find_args = ["find", ".subversion", "-type", "f", "-exec", "grep", "-q", realm, "{}", ";", "-print"]
+        find_output = self.run(find_args, cwd=home_directory, error_handler=Executive.ignore_error).rstrip()
+        if not find_output or not os.path.isfile(os.path.join(home_directory, find_output)):
+            return False
+        # Subversion either stores the password in the credential file, indicated by the presence of the key "password",
+        # or uses the system password store (e.g. Keychain on Mac OS X) as indicated by the presence of the key "passtype".
+        # We assume that these keys will not coincide with the actual credential data (e.g. that a person's username
+        # isn't "password") so that we can use grep.
+        if self.run(["grep", "password", find_output], cwd=home_directory, return_exit_code=True) == 0:
+            return True
+        return self.run(["grep", "passtype", find_output], cwd=home_directory, return_exit_code=True) == 0
+
+
+class SVN(SCM, SVNRepository):
+    # FIXME: These belong in common.config.urls
+    svn_server_host = "svn.webkit.org"
+    svn_server_realm = "<http://svn.webkit.org:80> Mac OS Forge"
+
+    executable_name = "svn"
+
+    _svn_metadata_files = frozenset(['.svn', '_svn'])
+
+    def __init__(self, cwd, patch_directories, **kwargs):
+        SCM.__init__(self, cwd, **kwargs)
+        self._bogus_dir = None
+        if patch_directories == []:
+            # FIXME: ScriptError is for Executive, this should probably be a normal Exception.
+            raise ScriptError(script_args=svn_info_args, message='Empty list of patch directories passed to SCM.__init__')
+        elif patch_directories == None:
+            self._patch_directories = [self._filesystem.relpath(cwd, self.checkout_root)]
+        else:
+            self._patch_directories = patch_directories
+
+    @classmethod
+    def in_working_directory(cls, path, executive=None):
+        if os.path.isdir(os.path.join(path, '.svn')):
+            # This is a fast shortcut for svn info that is usually correct for SVN < 1.7,
+            # but doesn't work for SVN >= 1.7.
+            return True
+
+        executive = executive or Executive()
+        svn_info_args = [cls.executable_name, 'info']
+        exit_code = executive.run_command(svn_info_args, cwd=path, return_exit_code=True)
+        return (exit_code == 0)
+
+    def find_uuid(self, path):
+        if not self.in_working_directory(path):
+            return None
+        return self.value_from_svn_info(path, 'Repository UUID')
+
+    @classmethod
+    def value_from_svn_info(cls, path, field_name):
+        svn_info_args = [cls.executable_name, 'info']
+        # FIXME: This method should use a passed in executive or be made an instance method and use self._executive.
+        info_output = Executive().run_command(svn_info_args, cwd=path).rstrip()
+        match = re.search("^%s: (?P<value>.+)$" % field_name, info_output, re.MULTILINE)
+        if not match:
+            raise ScriptError(script_args=svn_info_args, message='svn info did not contain a %s.' % field_name)
+        return match.group('value')
+
+    def find_checkout_root(self, path):
+        uuid = self.find_uuid(path)
+        # If |path| is not in a working directory, we're supposed to return |path|.
+        if not uuid:
+            return path
+        # Search up the directory hierarchy until we find a different UUID.
+        last_path = None
+        while True:
+            if uuid != self.find_uuid(path):
+                return last_path
+            last_path = path
+            (path, last_component) = self._filesystem.split(path)
+            if last_path == path:
+                return None
+
+    @staticmethod
+    def commit_success_regexp():
+        return "^Committed revision (?P<svn_revision>\d+)\.$"
+
+    def _run_svn(self, args, **kwargs):
+        return self.run([self.executable_name] + args, **kwargs)
+
+    @memoized
+    def svn_version(self):
+        return self._run_svn(['--version', '--quiet'])
+
+    def working_directory_is_clean(self):
+        return self._run_svn(["diff"], cwd=self.checkout_root, decode_output=False) == ""
+
+    def clean_working_directory(self):
+        # Make sure there are no locks lying around from a previously aborted svn invocation.
+        # This is slightly dangerous, as it's possible the user is running another svn process
+        # on this checkout at the same time.  However, it's much more likely that we're running
+        # under windows and svn just sucks (or the user interrupted svn and it failed to clean up).
+        self._run_svn(["cleanup"], cwd=self.checkout_root)
+
+        # svn revert -R is not as awesome as git reset --hard.
+        # It will leave added files around, causing later svn update
+        # calls to fail on the bots.  We make this mirror git reset --hard
+        # by deleting any added files as well.
+        added_files = reversed(sorted(self.added_files()))
+        # added_files() returns directories for SVN, we walk the files in reverse path
+        # length order so that we remove files before we try to remove the directories.
+        self._run_svn(["revert", "-R", "."], cwd=self.checkout_root)
+        for path in added_files:
+            # This is robust against cwd != self.checkout_root
+            absolute_path = self.absolute_path(path)
+            # Completely lame that there is no easy way to remove both types with one call.
+            if os.path.isdir(path):
+                os.rmdir(absolute_path)
+            else:
+                os.remove(absolute_path)
+
+    def status_command(self):
+        return [self.executable_name, 'status']
+
+    def _status_regexp(self, expected_types):
+        field_count = 6 if self.svn_version() > "1.6" else 5
+        return "^(?P<status>[%s]).{%s} (?P<filename>.+)$" % (expected_types, field_count)
+
+    def _add_parent_directories(self, path):
+        """Does 'svn add' to the path and its parents."""
+        if self.in_working_directory(path):
+            return
+        self.add(path)
+
+    def add_list(self, paths, return_exit_code=False):
+        for path in paths:
+            self._add_parent_directories(os.path.dirname(os.path.abspath(path)))
+        return self._run_svn(["add"] + paths, return_exit_code=return_exit_code)
+
+    def _delete_parent_directories(self, path):
+        if not self.in_working_directory(path):
+            return
+        if set(os.listdir(path)) - self._svn_metadata_files:
+            return  # Directory has non-trivial files in it.
+        self.delete(path)
+
+    def delete_list(self, paths):
+        for path in paths:
+            abs_path = os.path.abspath(path)
+            parent, base = os.path.split(abs_path)
+            result = self._run_svn(["delete", "--force", base], cwd=parent)
+            self._delete_parent_directories(os.path.dirname(abs_path))
+        return result
+
+    def exists(self, path):
+        return not self._run_svn(["info", path], return_exit_code=True, decode_output=False)
+
+    def changed_files(self, git_commit=None):
+        status_command = [self.executable_name, "status"]
+        status_command.extend(self._patch_directories)
+        # ACDMR: Addded, Conflicted, Deleted, Modified or Replaced
+        return self.run_status_and_extract_filenames(status_command, self._status_regexp("ACDMR"))
+
+    def changed_files_for_revision(self, revision):
+        # As far as I can tell svn diff --summarize output looks just like svn status output.
+        # No file contents printed, thus utf-8 auto-decoding in self.run is fine.
+        status_command = [self.executable_name, "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 = []
+        # svn log will exit(1) (and thus self.run will raise) if the path does not exist.
+        log_command = ['log', '--quiet', '--limit=%s' % limit, path]
+        for line in self._run_svn(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"))
+
+    def added_files(self):
+        return self.run_status_and_extract_filenames(self.status_command(), self._status_regexp("A"))
+
+    def deleted_files(self):
+        return self.run_status_and_extract_filenames(self.status_command(), self._status_regexp("D"))
+
+    @staticmethod
+    def supports_local_commits():
+        return False
+
+    def display_name(self):
+        return "svn"
+
+    def svn_revision(self, path):
+        return self.value_from_svn_info(path, 'Revision')
+
+    # FIXME: This method should be on Checkout.
+    def create_patch(self, git_commit=None, changed_files=None):
+        """Returns a byte array (str()) representing the patch file.
+        Patch files are effectively binary since they may contain
+        files of multiple different encodings."""
+        if changed_files == []:
+            return ""
+        elif changed_files == None:
+            changed_files = []
+        return self.run([self.script_path("svn-create-patch")] + changed_files,
+            cwd=self.checkout_root, return_stderr=False,
+            decode_output=False)
+
+    def committer_email_for_revision(self, revision):
+        return self._run_svn(["propget", "svn:author", "--revprop", "-r", revision]).rstrip()
+
+    def contents_at_revision(self, path, revision):
+        """Returns a byte array (str()) containing the contents
+        of path @ revision in the repository."""
+        remote_path = "%s/%s" % (self._repository_url(), path)
+        return self._run_svn(["cat", "-r", revision, remote_path], decode_output=False)
+
+    def diff_for_revision(self, revision):
+        # FIXME: This should probably use cwd=self.checkout_root
+        return self._run_svn(['diff', '-c', revision])
+
+    def _bogus_dir_name(self):
+        if sys.platform.startswith("win"):
+            parent_dir = tempfile.gettempdir()
+        else:
+            parent_dir = sys.path[0]  # tempdir is not secure.
+        return os.path.join(parent_dir, "temp_svn_config")
+
+    def _setup_bogus_dir(self, log):
+        self._bogus_dir = self._bogus_dir_name()
+        if not os.path.exists(self._bogus_dir):
+            os.mkdir(self._bogus_dir)
+            self._delete_bogus_dir = True
+        else:
+            self._delete_bogus_dir = False
+        if log:
+            log.debug('  Html: temp config dir: "%s".', self._bogus_dir)
+
+    def _teardown_bogus_dir(self, log):
+        if self._delete_bogus_dir:
+            shutil.rmtree(self._bogus_dir, True)
+            if log:
+                log.debug('  Html: removed temp config dir: "%s".', self._bogus_dir)
+        self._bogus_dir = None
+
+    def diff_for_file(self, path, log=None):
+        self._setup_bogus_dir(log)
+        try:
+            args = ['diff']
+            if self._bogus_dir:
+                args += ['--config-dir', self._bogus_dir]
+            args.append(path)
+            return self._run_svn(args, cwd=self.checkout_root)
+        finally:
+            self._teardown_bogus_dir(log)
+
+    def show_head(self, path):
+        return self._run_svn(['cat', '-r', 'BASE', path], decode_output=False)
+
+    def _repository_url(self):
+        return self.value_from_svn_info(self.checkout_root, 'URL')
+
+    def apply_reverse_diff(self, revision):
+        # '-c -revision' applies the inverse diff of 'revision'
+        svn_merge_args = ['merge', '--non-interactive', '-c', '-%s' % revision, self._repository_url()]
+        log("WARNING: svn merge has been known to take more than 10 minutes to complete.  It is recommended you use git for rollouts.")
+        log("Running 'svn %s'" % " ".join(svn_merge_args))
+        # FIXME: Should this use cwd=self.checkout_root?
+        self._run_svn(svn_merge_args)
+
+    def revert_files(self, file_paths):
+        # FIXME: This should probably use cwd=self.checkout_root.
+        self._run_svn(['revert'] + file_paths)
+
+    def commit_with_message(self, message, username=None, password=None, git_commit=None, force_squash=False, changed_files=None):
+        # git-commit and force are not used by SVN.
+        svn_commit_args = ["commit"]
+
+        if not username and not self.has_authorization_for_realm(self.svn_server_realm):
+            raise AuthenticationError(self.svn_server_host)
+        if username:
+            svn_commit_args.extend(["--username", username])
+
+        svn_commit_args.extend(["-m", message])
+
+        if changed_files:
+            svn_commit_args.extend(changed_files)
+
+        return self._run_svn(svn_commit_args, cwd=self.checkout_root, error_handler=commit_error_handler)
+
+    def svn_commit_log(self, svn_revision):
+        svn_revision = self.strip_r_from_svn_revision(svn_revision)
+        return self._run_svn(['log', '--non-interactive', '--revision', svn_revision])
+
+    def last_svn_commit_log(self):
+        # BASE is the checkout revision, HEAD is the remote repository revision
+        # http://svnbook.red-bean.com/en/1.0/ch03s03.html
+        return self.svn_commit_log('BASE')
+
+    def svn_blame(self, path):
+        return self._run_svn(['blame', path])
+
+    def propset(self, pname, pvalue, path):
+        dir, base = os.path.split(path)
+        return self._run_svn(['pset', pname, pvalue, base], cwd=dir)
+
+    def propget(self, pname, path):
+        dir, base = os.path.split(path)
+        return self._run_svn(['pget', pname, base], cwd=dir).encode('utf-8').rstrip("\n")
diff --git a/Tools/Scripts/webkitpy/common/checksvnconfigfile.py b/Tools/Scripts/webkitpy/common/checksvnconfigfile.py
new file mode 100644
index 0000000..e6165f6
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/checksvnconfigfile.py
@@ -0,0 +1,65 @@
+# Copyright (C) 2012 Balazs Ankes (bank@inf.u-szeged.hu) University of Szeged
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# This file is used by:
+# webkitpy/tool/steps/addsvnmimetypeforpng.py
+# webkitpy/style/checkers/png.py
+
+import os
+import re
+
+
+def check(host, fs):
+    """
+    check the svn config file
+    return with three logical value:
+    is svn config file missing, is auto-props missing, is the svn:mime-type for png missing
+    """
+
+    cfg_file_path = config_file_path(host, fs)
+
+    try:
+        config_file = fs.read_text_file(cfg_file_path)
+    except IOError:
+        return (True, True, True)
+
+    errorcode_autoprop = not re.search("^\s*enable-auto-props\s*=\s*yes", config_file, re.MULTILINE)
+    errorcode_png = not re.search("^\s*\*\.png\s*=\s*svn:mime-type=image/png", config_file, re.MULTILINE)
+
+    return (False, errorcode_autoprop, errorcode_png)
+
+
+def config_file_path(host, fs):
+    if host.platform.is_win():
+        config_file_path = fs.join(os.environ['APPDATA'], "Subversion", "config")
+    else:
+        config_file_path = fs.join(fs.expanduser("~"), ".subversion", "config")
+    return config_file_path
+
+
+def errorstr_autoprop(config_file_path):
+    return 'Have to enable auto props in the subversion config file (%s "enable-auto-props = yes"). ' % config_file_path
+
+
+def errorstr_png(config_file_path):
+    return 'Have to set the svn:mime-type in the subversion config file (%s "*.png = svn:mime-type=image/png").' % config_file_path
diff --git a/Tools/Scripts/webkitpy/common/config/__init__.py b/Tools/Scripts/webkitpy/common/config/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/config/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/common/config/build.py b/Tools/Scripts/webkitpy/common/config/build.py
new file mode 100644
index 0000000..2ecacc7
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/config/build.py
@@ -0,0 +1,135 @@
+# 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.
+
+"""Functions relating to building WebKit"""
+
+import re
+
+
+def _should_file_trigger_build(target_platform, file):
+    # The directories and patterns lists below map directory names or
+    # regexp patterns to the bot platforms for which they should trigger a
+    # build. Mapping to the empty list means that no builds should be
+    # triggered on any platforms. Earlier directories/patterns take
+    # precendence over later ones.
+
+    # FIXME: The patterns below have only been verified to be correct on
+    # the platforms listed below. We should implement this for other platforms
+    # and start using it for their bots. Someone familiar with each platform
+    # will have to figure out what the right set of directories/patterns is for
+    # that platform.
+    assert(target_platform in ("mac-leopard", "mac-lion", "mac-mountainlion", "mac-snowleopard", "win"))
+
+    directories = [
+        # Directories that shouldn't trigger builds on any bots.
+        ("Examples", []),
+        ("PerformanceTests", []),
+        ("ManualTests", []),
+        ("Tools/BuildSlaveSupport/build.webkit.org-config/public_html", []),
+        ("Websites", []),
+        ("efl", []),
+        ("iphone", []),
+        ("opengl", []),
+        ("opentype", []),
+        ("openvg", []),
+        ("wince", []),
+        ("wx", []),
+
+        # Directories that should trigger builds on only some bots.
+        ("Source/WebCore/image-decoders", ["chromium"]),
+        ("LayoutTests/platform/mac", ["mac", "win"]),
+        ("cairo", ["gtk", "wincairo"]),
+        ("cf", ["chromium-mac", "mac", "qt", "win"]),
+        ("chromium", ["chromium"]),
+        ("cocoa", ["chromium-mac", "mac"]),
+        ("curl", ["gtk", "wincairo"]),
+        ("gobject", ["gtk"]),
+        ("gpu", ["chromium", "mac"]),
+        ("gstreamer", ["gtk"]),
+        ("gtk", ["gtk"]),
+        ("mac", ["chromium-mac", "mac"]),
+        ("mac-leopard", ["mac-leopard"]),
+        ("mac-lion", ["mac-leopard", "mac-lion", "mac-snowleopard", "win"]),
+        ("mac-snowleopard", ["mac-leopard", "mac-snowleopard"]),
+        ("mac-wk2", ["mac-lion", "mac-snowleopard", "mac-mountainlion", "win"]),
+        ("objc", ["mac"]),
+        ("qt", ["qt"]),
+        ("skia", ["chromium"]),
+        ("soup", ["gtk"]),
+        ("v8", ["chromium"]),
+        ("win", ["chromium-win", "win"]),
+    ]
+    patterns = [
+        # Patterns that shouldn't trigger builds on any bots.
+        (r"(?:^|/)ChangeLog.*$", []),
+        (r"(?:^|/)Makefile$", []),
+        (r"/ARM", []),
+        (r"/CMake.*", []),
+        (r"/LICENSE[^/]+$", []),
+        (r"ARM(?:v7)?\.(?:cpp|h)$", []),
+        (r"MIPS\.(?:cpp|h)$", []),
+        (r"WinCE\.(?:cpp|h|mm)$", []),
+        (r"\.(?:bkl|mk)$", []),
+
+        # Patterns that should trigger builds on only some bots.
+        (r"(?:^|/)GNUmakefile\.am$", ["gtk"]),
+        (r"/\w+Chromium\w*\.(?:cpp|h|mm)$", ["chromium"]),
+        (r"Mac\.(?:cpp|h|mm)$", ["mac"]),
+        (r"\.(?:vcproj|vsprops|sln)$", ["win"]),
+        (r"\.exp(?:\.in)?$", ["mac"]),
+        (r"\.gypi?", ["chromium"]),
+        (r"\.order$", ["mac"]),
+        (r"\.pr[io]$", ["qt"]),
+        (r"\.vcproj/", ["win"]),
+        (r"\.xcconfig$", ["mac"]),
+        (r"\.xcodeproj/", ["mac"]),
+    ]
+
+    base_platform = target_platform.split("-")[0]
+
+    # See if the file is in one of the known directories.
+    for directory, platforms in directories:
+        if re.search(r"(?:^|/)%s/" % directory, file):
+            return target_platform in platforms or base_platform in platforms
+
+    # See if the file matches a known pattern.
+    for pattern, platforms in patterns:
+        if re.search(pattern, file):
+            return target_platform in platforms or base_platform in platforms
+
+    # See if the file is a platform-specific test result.
+    match = re.match("LayoutTests/platform/(?P<platform>[^/]+)/", file)
+    if match:
+        # See if the file is a test result for this platform, our base
+        # platform, or one of our sub-platforms.
+        return match.group("platform") in (target_platform, base_platform) or match.group("platform").startswith("%s-" % target_platform)
+
+    # The file isn't one we know about specifically, so we should assume we
+    # have to build.
+    return True
+
+
+def should_build(target_platform, changed_files):
+    """Returns true if the changed files affect the given platform, and
+    thus a build should be performed. target_platform should be one of the
+    platforms used in the build.webkit.org master's config.json file."""
+    return any(_should_file_trigger_build(target_platform, file) for file in changed_files)
diff --git a/Tools/Scripts/webkitpy/common/config/build_unittest.py b/Tools/Scripts/webkitpy/common/config/build_unittest.py
new file mode 100644
index 0000000..c496179
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/config/build_unittest.py
@@ -0,0 +1,70 @@
+# 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.
+
+import unittest
+
+from webkitpy.common.config import build
+
+
+class ShouldBuildTest(unittest.TestCase):
+    _should_build_tests = [
+        (["ChangeLog", "Source/WebCore/ChangeLog", "Source/WebKit2/ChangeLog-2011-02-11"], []),
+        (["GNUmakefile.am", "Source/WebCore/GNUmakefile.am"], ["gtk"]),
+        (["Websites/bugs.webkit.org/foo", "Source/WebCore/bar"], ["*"]),
+        (["Websites/bugs.webkit.org/foo"], []),
+        (["Source/JavaScriptCore/JavaScriptCore.xcodeproj/foo"], ["mac-leopard", "mac-lion",  "mac-mountainlion", "mac-snowleopard"]),
+        (["Source/JavaScriptCore/JavaScriptCore.vcproj/foo", "Source/WebKit2/win/WebKit2.vcproj", "Source/WebKit/win/WebKit.sln", "Tools/WebKitTestRunner/Configurations/WebKitTestRunnerCommon.vsprops"], ["win"]),
+        (["LayoutTests/platform/mac/foo", "Source/WebCore/bar"], ["*"]),
+        (["LayoutTests/foo"], ["*"]),
+        (["LayoutTests/canvas/philip/tests/size.attributes.parse.exp-expected.txt", "LayoutTests/canvas/philip/tests/size.attributes.parse.exp.html"], ["*"]),
+        (["LayoutTests/platform/chromium-linux/foo"], ["chromium-linux"]),
+        (["LayoutTests/platform/chromium-win/fast/compact/001-expected.txt"], ["chromium-win"]),
+        (["LayoutTests/platform/mac-leopard/foo"], ["mac-leopard"]),
+        (["LayoutTests/platform/mac-lion/foo"], ["mac-leopard", "mac-lion", "mac-snowleopard", "win"]),
+        (["LayoutTests/platform/mac-snowleopard/foo"], ["mac-leopard", "mac-snowleopard"]),
+        (["LayoutTests/platform/mac-wk2/Skipped"], ["mac-lion",  "mac-mountainlion", "mac-snowleopard", "win"]),
+        (["LayoutTests/platform/mac/foo"], ["mac-leopard", "mac-lion", "mac-mountainlion", "mac-snowleopard", "win"]),
+        (["LayoutTests/platform/win-xp/foo"], ["win"]),
+        (["LayoutTests/platform/win-wk2/foo"], ["win"]),
+        (["LayoutTests/platform/win/foo"], ["win"]),
+        (["Source/WebCore.exp.in", "Source/WebKit/mac/WebKit.exp"], ["mac-leopard", "mac-lion",  "mac-mountainlion", "mac-snowleopard"]),
+        (["Source/WebCore/mac/foo"], ["chromium-mac", "mac-leopard", "mac-lion",  "mac-mountainlion", "mac-snowleopard"]),
+        (["Source/WebCore/win/foo"], ["chromium-win", "win"]),
+        (["Source/WebCore/platform/graphics/gpu/foo"], ["mac-leopard", "mac-lion",  "mac-mountainlion", "mac-snowleopard"]),
+        (["Source/WebCore/platform/wx/wxcode/win/foo"], []),
+        (["Source/WebCore/rendering/RenderThemeMac.mm", "Source/WebCore/rendering/RenderThemeMac.h"], ["mac-leopard", "mac-lion",  "mac-mountainlion", "mac-snowleopard"]),
+        (["Source/WebCore/rendering/RenderThemeChromiumLinux.h"], ["chromium-linux"]),
+        (["Source/WebCore/rendering/RenderThemeWinCE.h"], []),
+        (["Tools/BuildSlaveSupport/build.webkit.org-config/public_html/LeaksViewer/LeaksViewer.js"], []),
+    ]
+
+    def test_should_build(self):
+        for files, platforms in self._should_build_tests:
+            # FIXME: We should test more platforms here once
+            # build._should_file_trigger_build is implemented for them.
+            for platform in ["mac-leopard", "mac-lion",  "mac-mountainlion", "mac-snowleopard", "win"]:
+                should_build = platform in platforms or "*" in platforms
+                self.assertEqual(build.should_build(platform, files), should_build, "%s should%s have built but did%s (files: %s)" % (platform, "" if should_build else "n't", "n't" if should_build else "", str(files)))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/common/config/committers.py b/Tools/Scripts/webkitpy/common/config/committers.py
new file mode 100644
index 0000000..36df3db
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/config/committers.py
@@ -0,0 +1,717 @@
+# Copyright (c) 2011, Apple Inc. All rights reserved.
+# Copyright (c) 2009, 2011, 2012 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.
+#
+# WebKit's Python module for committer and reviewer validation.
+
+from webkitpy.common.editdistance import edit_distance
+
+class Account(object):
+    def __init__(self, name, email_or_emails, irc_nickname_or_nicknames=None):
+        assert(name)
+        assert(email_or_emails)
+        self.full_name = name
+        if isinstance(email_or_emails, str):
+            self.emails = [email_or_emails]
+        else:
+            self.emails = email_or_emails
+        self.emails = map(lambda email: email.lower(), self.emails)  # Emails are case-insensitive.
+        if isinstance(irc_nickname_or_nicknames, str):
+            self.irc_nicknames = [irc_nickname_or_nicknames]
+        else:
+            self.irc_nicknames = irc_nickname_or_nicknames
+        self.can_commit = False
+        self.can_review = False
+
+    def bugzilla_email(self):
+        # FIXME: We're assuming the first email is a valid bugzilla email,
+        # which might not be right.
+        return self.emails[0]
+
+    def __str__(self):
+        return '"%s" <%s>' % (self.full_name, self.emails[0])
+
+    def contains_string(self, search_string):
+        string = search_string.lower()
+        if string in self.full_name.lower():
+            return True
+        if self.irc_nicknames:
+            for nickname in self.irc_nicknames:
+                if string in nickname.lower():
+                    return True
+        for email in self.emails:
+            if string in email:
+                return True
+        return False
+
+
+class Contributor(Account):
+    def __init__(self, name, email_or_emails, irc_nickname=None):
+        Account.__init__(self, name, email_or_emails, irc_nickname)
+        self.is_contributor = True
+
+
+class Committer(Contributor):
+    def __init__(self, name, email_or_emails, irc_nickname=None):
+        Contributor.__init__(self, name, email_or_emails, irc_nickname)
+        self.can_commit = True
+
+
+class Reviewer(Committer):
+    def __init__(self, name, email_or_emails, irc_nickname=None):
+        Committer.__init__(self, name, email_or_emails, irc_nickname)
+        self.can_review = True
+
+
+# This is a list of email addresses that have bugzilla accounts but are not
+# used for contributing (such as mailing lists).
+
+
+watchers_who_are_not_contributors = [
+    Account("Chromium Compositor Bugs", ["cc-bugs@chromium.org"], ""),
+    Account("Chromium Media Reviews", ["feature-media-reviews@chromium.org"], ""),
+    Account("David Levin", ["levin+threading@chromium.org"], ""),
+    Account("David Levin", ["levin+watchlist@chromium.org"], ""),
+    Account("Kent Tamura", ["tkent+wkapi@chromium.org"], ""),
+]
+
+
+# This is a list of people (or bots) who are neither committers nor reviewers, but get
+# frequently CC'ed by others on Bugzilla bugs, so their names should be
+# supported by autocomplete. No review needed to add to the list.
+
+
+contributors_who_are_not_committers = [
+    Contributor("Adobe Bug Tracker", "WebkitBugTracker@adobe.com"),
+    Contributor("Aharon Lanin", "aharon@google.com"),
+    Contributor("Alan Stearns", "stearns@adobe.com", "astearns"),
+    Contributor("Alejandro Pineiro", "apinheiro@igalia.com"),
+    Contributor("Alexey Marinichev", ["amarinichev@chromium.org", "amarinichev@google.com"], "amarinichev"),
+    Contributor("Andras Piroska", "pandras@inf.u-szeged.hu", "andris88"),
+    Contributor("Andrei Bucur", "abucur@adobe.com", "abucur"),
+    Contributor("Anne van Kesteren", "annevankesteren+webkit@gmail.com", "annevk"),
+    Contributor("Annie Sullivan", "sullivan@chromium.org", "annie"),
+    Contributor("Aryeh Gregor", "ayg@aryeh.name", "AryehGregor"),
+    Contributor("Balazs Ankes", "bank@inf.u-szeged.hu", "abalazs"),
+    Contributor("Brian Salomon", "bsalomon@google.com"),
+    Contributor("Commit Queue", "commit-queue@webkit.org"),
+    Contributor("Daniel Sievers", "sievers@chromium.org"),
+    Contributor("David Dorwin", "ddorwin@chromium.org", "ddorwin"),
+    Contributor("David Reveman", "reveman@chromium.org", "reveman"),
+    Contributor("Dongsung Huang", "luxtella@company100.net", "Huang"),
+    Contributor("Douglas Davidson", "ddavidso@apple.com"),
+    Contributor("Edward O'Connor", "eoconnor@apple.com", "hober"),
+    Contributor("Elliott Sprehn", "esprehn@chromium.org", "esprehn"),
+    Contributor("Eric Penner", "epenner@chromium.org", "epenner"),
+    Contributor("Felician Marton", ["felician@inf.u-szeged.hu", "marton.felician.zoltan@stud.u-szeged.hu"], "Felician"),
+    Contributor("Finnur Thorarinsson", ["finnur@chromium.org", "finnur.webkit@gmail.com"], "finnur"),
+    Contributor("Forms Bugs", "forms-bugs@chromium.org"),
+    Contributor("Glenn Adams", "glenn@skynav.com", "gasubic"),
+    Contributor("Gabor Ballabas", "gaborb@inf.u-szeged.hu", "bgabor"),
+    Contributor("Grace Kloba", "klobag@chromium.org", "klobag"),
+    Contributor("Greg Simon", "gregsimon@chromium.org", "gregsimon"),
+    Contributor("Gregg Tavares", ["gman@google.com", "gman@chromium.org"], "gman"),
+    Contributor("Hao Zheng", "zhenghao@chromium.org"),
+    Contributor("Harald Alvestrand", "hta@google.com", "hta"),
+    Contributor("Ian Hickson", "ian@hixie.ch", "hixie"),
+    Contributor("Janos Badics", "jbadics@inf.u-szeged.hu", "dicska"),
+    Contributor("Jonathan Backer", "backer@chromium.org", "backer"),
+    Contributor("Jeff Timanus", ["twiz@chromium.org", "twiz@google.com"], "twiz"),
+    Contributor("Jing Zhao", "jingzhao@chromium.org"),
+    Contributor("Joanmarie Diggs", "jdiggs@igalia.com"),
+    Contributor("John Bates", ["jbates@google.com", "jbates@chromium.org"], "jbates"),
+    Contributor("John Bauman", ["jbauman@chromium.org", "jbauman@google.com"], "jbauman"),
+    Contributor("John Mellor", "johnme@chromium.org", "johnme"),
+    Contributor("Kulanthaivel Palanichamy", "kulanthaivel@codeaurora.org", "kvel"),
+    Contributor("Kiran Muppala", "cmuppala@apple.com", "kiranm"),
+    Contributor("Mihai Balan", "mibalan@adobe.com", "miChou"),
+    Contributor("Min Qin", "qinmin@chromium.org"),
+    Contributor("Nandor Huszka", "hnandor@inf.u-szeged.hu", "hnandor"),
+    Contributor("Oliver Varga", ["voliver@inf.u-szeged.hu", "Varga.Oliver@stud.u-szeged.hu"], "TwistO"),
+    Contributor("Peter Gal", "galpeter@inf.u-szeged.hu", "elecro"),
+    Contributor("Peter Linss", "peter.linss@hp.com", "plinss"),
+    Contributor("Pravin D", "pravind.2k4@gmail.com", "pravind"),
+    Contributor("Radar WebKit Bug Importer", "webkit-bug-importer@group.apple.com"),
+    Contributor("Raul Hudea", "rhudea@adobe.com", "rhudea"),
+    Contributor("Roland Takacs", "rtakacs@inf.u-szeged.hu", "rtakacs"),
+    Contributor(u"Sami Ky\u00f6stil\u00e4", "skyostil@chromium.org", "skyostil"),
+    Contributor("Szilard Ledan-Muntean", "szledan@inf.u-szeged.hu", "szledan"),
+    Contributor("Tab Atkins", ["tabatkins@google.com", "jackalmage@gmail.com"], "tabatkins"),
+    Contributor("Tamas Czene", ["tczene@inf.u-szeged.hu", "Czene.Tamas@stud.u-szeged.hu"], "tczene"),
+    Contributor("Tien-Ren Chen", "trchen@chromium.org", "trchen"),
+    Contributor("WebKit Review Bot", "webkit.review.bot@gmail.com", "sheriff-bot"),
+    Contributor("Web Components Team", "webcomponents-bugzilla@chromium.org"),
+    Contributor("Wyatt Carss", ["wcarss@chromium.org", "wcarss@google.com"], "wcarss"),
+    Contributor("Zeev Lieber", "zlieber@chromium.org"),
+    Contributor("Zoltan Arvai", "zarvai@inf.u-szeged.hu", "azbest_hu"),
+    Contributor("Zsolt Feher", "feherzs@inf.u-szeged.hu", "Smith"),
+]
+
+
+# This is intended as a canonical, machine-readable list of all non-reviewer
+# committers for WebKit.  If your name is missing here and you are a committer,
+# please add it.  No review needed.  All reviewers are committers, so this list
+# is only of committers who are not reviewers.
+
+
+committers_unable_to_review = [
+    Committer("Aaron Boodman", "aa@chromium.org", "aboodman"),
+    Committer("Adam Bergkvist", "adam.bergkvist@ericsson.com", "adambe"),
+    Committer("Adam Kallai", "kadam@inf.u-szeged.hu", "kadam"),
+    Committer("Adam Klein", "adamk@chromium.org", "aklein"),
+    Committer("Adam Langley", "agl@chromium.org", "agl"),
+    Committer("Ademar de Souza Reis Jr", ["ademar.reis@gmail.com", "ademar@webkit.org"], "ademar"),
+    Committer("Albert J. Wong", "ajwong@chromium.org"),
+    Committer("Alec Flett", ["alecflett@chromium.org", "alecflett@google.com"], "alecf"),
+    Committer(u"Alexander F\u00e6r\u00f8y", ["ahf@0x90.dk", "alexander.faeroy@nokia.com"], "ahf"),
+    Committer("Alexander Kellett", ["lypanov@mac.com", "a-lists001@lypanov.net", "lypanov@kde.org"], "lypanov"),
+    Committer("Alexandre Elias", ["aelias@chromium.org", "aelias@google.com"], "aelias"),
+    Committer("Alexandru Chiculita", "achicu@adobe.com", "achicu"),
+    Committer("Alice Boxhall", "aboxhall@chromium.org", "aboxhall"),
+    Committer("Allan Sandfeld Jensen", ["allan.jensen@digia.com", "kde@carewolf.com", "sandfeld@kde.org", "allan.jensen@nokia.com"], "carewolf"),
+    Committer("Alok Priyadarshi", "alokp@chromium.org", "alokp"),
+    Committer("Ami Fischman", ["fischman@chromium.org", "fischman@google.com"], "fischman"),
+    Committer("Amruth Raj", "amruthraj@motorola.com", "amruthraj"),
+    Committer("Andre Boule", "aboule@apple.com"),
+    Committer("Andrei Popescu", "andreip@google.com", "andreip"),
+    Committer("Andrew Wellington", ["andrew@webkit.org", "proton@wiretapped.net"], "proton"),
+    Committer("Andrew Scherkus", "scherkus@chromium.org", "scherkus"),
+    Committer("Andrey Kosyakov", "caseq@chromium.org", "caseq"),
+    Committer("Andras Becsi", ["abecsi@webkit.org", "andras.becsi@digia.com"], "bbandix"),
+    Committer("Andy Wingo", "wingo@igalia.com", "wingo"),
+    Committer("Anna Cavender", "annacc@chromium.org", "annacc"),
+    Committer("Anthony Ricaud", "rik@webkit.org", "rik"),
+    Committer("Antoine Labour", "piman@chromium.org", "piman"),
+    Committer("Anton D'Auria", "adauria@apple.com", "antonlefou"),
+    Committer("Anton Muhin", "antonm@chromium.org", "antonm"),
+    Committer("Arko Saha", "arko@motorola.com", "arkos"),
+    Committer("Arvid Nilsson", "anilsson@rim.com", "anilsson"),
+    Committer("Balazs Kelemen", "kbalazs@webkit.org", "kbalazs"),
+    Committer("Ben Murdoch", "benm@google.com", "benm"),
+    Committer("Ben Wells", "benwells@chromium.org", "benwells"),
+    Committer("Benjamin C Meyer", ["ben@meyerhome.net", "ben@webkit.org", "bmeyer@rim.com"], "icefox"),
+    Committer("Benjamin Kalman", ["kalman@chromium.org", "kalman@google.com"], "kalman"),
+    Committer("Benjamin Otte", ["otte@gnome.org", "otte@webkit.org"], "otte"),
+    Committer("Bill Budge", ["bbudge@chromium.org", "bbudge@gmail.com"], "bbudge"),
+    Committer("Brett Wilson", "brettw@chromium.org", "brettx"),
+    Committer("Bruno de Oliveira Abinader", ["bruno.abinader@basyskom.com", "brunoabinader@gmail.com"], "abinader"),
+    Committer("Cameron McCormack", ["cam@mcc.id.au", "cam@webkit.org"], "heycam"),
+    Committer("Carol Szabo", ["carol@webkit.org", "carol.szabo@nokia.com"], "cszabo1"),
+    Committer("Cary Clark", ["caryclark@google.com", "caryclark@chromium.org"], "caryclark"),
+    Committer("Charles Reis", "creis@chromium.org", "creis"),
+    Committer("Charles Wei", ["charles.wei@torchmobile.com.cn"], "cswei"),
+    Committer("Chris Evans", ["cevans@google.com", "cevans@chromium.org"]),
+    Committer("Chris Guillory", ["ctguil@chromium.org", "chris.guillory@google.com"], "ctguil"),
+    Committer("Chris Petersen", "cpetersen@apple.com", "cpetersen"),
+    Committer("Christian Dywan", ["christian@twotoasts.de", "christian@webkit.org", "christian@lanedo.com"]),
+    Committer("Collin Jackson", "collinj@webkit.org", "collinjackson"),
+    Committer("Cris Neckar", "cdn@chromium.org", "cneckar"),
+    Committer("Dan Winship", "danw@gnome.org", "danw"),
+    Committer("Dana Jansens", "danakj@chromium.org", "danakj"),
+    Committer("Daniel Cheng", "dcheng@chromium.org", "dcheng"),
+    Committer("Dave Barton", "dbarton@mathscribe.com", "davebarton"),
+    Committer("Dave Tharp", "dtharp@codeaurora.org", "dtharp"),
+    Committer("David Michael Barr", ["davidbarr@chromium.org", "davidbarr@google.com", "b@rr-dav.id.au"], "barrbrain"),
+    Committer("David Grogan", ["dgrogan@chromium.org", "dgrogan@google.com"], "dgrogan"),
+    Committer("David Smith", ["catfish.man@gmail.com", "dsmith@webkit.org"], "catfishman"),
+    Committer("Diego Gonzalez", ["diegohcg@webkit.org", "diego.gonzalez@openbossa.org"], "diegohcg"),
+    Committer("Dinu Jacob", "dinu.s.jacob@intel.com", "dsjacob"),
+    Committer("Dmitry Lomov", ["dslomov@google.com", "dslomov@chromium.org"], "dslomov"),
+    Committer("Dominic Cooney", ["dominicc@chromium.org", "dominicc@google.com"], "dominicc"),
+    Committer("Dominic Mazzoni", ["dmazzoni@google.com", "dmazzoni@chromium.org"], "dmazzoni"),
+    Committer(u"Dominik R\u00f6ttsches", ["dominik.rottsches@intel.com", "d-r@roettsches.de"], "drott"),
+    Committer("Drew Wilson", "atwilson@chromium.org", "atwilson"),
+    Committer("Eli Fidler", ["eli@staikos.net", "efidler@rim.com"], "efidler"),
+    Committer("Elliot Poger", "epoger@chromium.org", "epoger"),
+    Committer("Erik Arvidsson", "arv@chromium.org", "arv"),
+    Committer("Eric Roman", "eroman@chromium.org", "eroman"),
+    Committer("Eric Uhrhane", "ericu@chromium.org", "ericu"),
+    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("Florin Malita", ["fmalita@chromium.org", "fmalita@google.com"], "fmalita"),
+    Committer("Fumitoshi Ukai", "ukai@chromium.org", "ukai"),
+    Committer("Gabor Loki", "loki@webkit.org", "loki04"),
+    Committer("Gabor Rapcsanyi", ["rgabor@webkit.org", "rgabor@inf.u-szeged.hu"], "rgabor"),
+    Committer("Gavin Peters", ["gavinp@chromium.org", "gavinp@webkit.org", "gavinp@google.com"], "gavinp"),
+    Committer("Girish Ramakrishnan", ["girish@forwardbias.in", "ramakrishnan.girish@gmail.com"], "girishr"),
+    Committer("Graham Dennis", ["Graham.Dennis@gmail.com", "gdennis@webkit.org"]),
+    Committer("Greg Bolsinga", "bolsinga@apple.com"),
+    Committer("Grzegorz Czajkowski", "g.czajkowski@samsung.com", "grzegorz"),
+    Committer("Hans Wennborg", "hans@chromium.org", "hwennborg"),
+    Committer("Hayato Ito", "hayato@chromium.org", "hayato"),
+    Committer("Hironori Bono", "hbono@chromium.org", "hbono"),
+    Committer("Helder Correia", "helder.correia@nokia.com", "helder"),
+    Committer("Hin-Chung Lam", ["hclam@google.com", "hclam@chromium.org"]),
+    Committer("Hugo Parente Lima", "hugo.lima@openbossa.org", "hugopl"),
+    Committer("Ian Vollick", "vollick@chromium.org", "vollick"),
+    Committer("Igor Trindade Oliveira", ["igor.oliveira@webkit.org", "igor.o@sisa.samsung.com"], "igoroliveira"),
+    Committer("Ilya Sherman", "isherman@chromium.org", "isherman"),
+    Committer("Ilya Tikhonovsky", "loislo@chromium.org", "loislo"),
+    Committer("Ivan Krsti\u0107", "ike@apple.com"),
+    Committer("Jacky Jiang", ["jkjiang@webkit.org", "zkjiang008@gmail.com", "zhajiang@rim.com"], "jkjiang"),
+    Committer("Jakob Petsovits", ["jpetsovits@rim.com", "jpetso@gmx.at"], "jpetso"),
+    Committer("Jakub Wieczorek", "jwieczorek@webkit.org", "fawek"),
+    Committer("James Hawkins", ["jhawkins@chromium.org", "jhawkins@google.com"], "jhawkins"),
+    Committer("James Kozianski", ["koz@chromium.org", "koz@google.com"], "koz"),
+    Committer("James Simonsen", "simonjam@chromium.org", "simonjam"),
+    Committer("Jarred Nicholls", ["jarred@webkit.org", "jarred@sencha.com"], "jarrednicholls"),
+    Committer("Jason Liu", ["jason.liu@torchmobile.com.cn", "jasonliuwebkit@gmail.com"], "jasonliu"),
+    Committer("Jay Civelli", "jcivelli@chromium.org", "jcivelli"),
+    Committer("Jeff Miller", "jeffm@apple.com", "jeffm7"),
+    Committer("Jeffrey Pfau", ["jeffrey@endrift.com", "jpfau@apple.com"], "jpfau"),
+    Committer("Jenn Braithwaite", "jennb@chromium.org", "jennb"),
+    Committer("Jens Alfke", ["snej@chromium.org", "jens@apple.com"]),
+    Committer("Jer Noble", "jer.noble@apple.com", "jernoble"),
+    Committer("Jeremy Moskovich", ["playmobil@google.com", "jeremy@chromium.org"], "jeremymos"),
+    Committer("Jesus Sanchez-Palencia", ["jesus@webkit.org", "jesus.palencia@openbossa.org"], "jeez_"),
+    Committer("Jia Pu", "jpu@apple.com"),
+    Committer("Joe Thomas", "joethomas@motorola.com", "joethomas"),
+    Committer("John Abd-El-Malek", "jam@chromium.org", "jam"),
+    Committer("John Gregg", ["johnnyg@google.com", "johnnyg@chromium.org"], "johnnyg"),
+    Committer("John Knottenbelt", "jknotten@chromium.org", "jknotten"),
+    Committer("Johnny Ding", ["jnd@chromium.org", "johnnyding.webkit@gmail.com"], "johnnyding"),
+    Committer("Jon Lee", "jonlee@apple.com", "jonlee"),
+    Committer("Jonathan Dong", ["jonathan.dong@torchmobile.com.cn"], "jondong"),
+    Committer("Joone Hur", ["joone@webkit.org", "joone.hur@intel.com"], "joone"),
+    Committer("Joost de Valk", ["joost@webkit.org", "webkit-dev@joostdevalk.nl"], "Altha"),
+    Committer("Joshua Bell", ["jsbell@chromium.org", "jsbell@google.com"], "jsbell"),
+    Committer("Julie Parent", ["jparent@google.com", "jparent@chromium.org"], "jparent"),
+    Committer("Jungshik Shin", "jshin@chromium.org"),
+    Committer("Justin Novosad", ["junov@google.com", "junov@chromium.org"], "junov"),
+    Committer("Justin Schuh", "jschuh@chromium.org", "jschuh"),
+    Committer("Kaustubh Atrawalkar", ["kaustubh@motorola.com"], "silverroots"),
+    Committer("Keishi Hattori", "keishi@webkit.org", "keishi"),
+    Committer("Kelly Norton", "knorton@alum.mit.edu"),
+    Committer("Ken Buchanan", "kenrb@chromium.org", "kenrb"),
+    Committer("Kenichi Ishibashi", "bashi@chromium.org", "bashi"),
+    Committer("Kenji Imasaki", "imasaki@chromium.org", "imasaki"),
+    Committer("Kent Hansen", "kent.hansen@nokia.com", "khansen"),
+    Committer("Kihong Kwon", "kihong.kwon@samsung.com", "kihong"),
+    Committer(u"Kim Gr\u00f6nholm", "kim.1.gronholm@nokia.com"),
+    Committer("Kimmo Kinnunen", ["kimmo.t.kinnunen@nokia.com", "kimmok@iki.fi", "ktkinnun@webkit.org"], "kimmok"),
+    Committer("Kinuko Yasuda", "kinuko@chromium.org", "kinuko"),
+    Committer("Konrad Piascik", "kpiascik@rim.com", "kpiascik"),
+    Committer("Kristof Kosztyo", "kkristof@inf.u-szeged.hu", "kkristof"),
+    Committer("Krzysztof Kowalczyk", "kkowalczyk@gmail.com"),
+    Committer("Kwang Yul Seo", ["skyul@company100.net", "kseo@webkit.org"], "kseo"),
+    Committer("Lauro Neto", "lauro.neto@openbossa.org", "lmoura"),
+    Committer("Leandro Gracia Gil", "leandrogracia@chromium.org", "leandrogracia"),
+    Committer("Leandro Pereira", ["leandro@profusion.mobi", "leandro@webkit.org"], "acidx"),
+    Committer("Leo Yang", ["leoyang@rim.com", "leoyang@webkit.org", "leoyang.webkit@gmail.com"], "leoyang"),
+    Committer("Lucas De Marchi", ["demarchi@webkit.org", "lucas.demarchi@profusion.mobi"], "demarchi"),
+    Committer("Lucas Forschler", ["lforschler@apple.com"], "lforschler"),
+    Committer("Luciano Wolf", "luciano.wolf@openbossa.org", "luck"),
+    Committer("Luke Macpherson", ["macpherson@chromium.org", "macpherson@google.com"], "macpherson"),
+    Committer("Mads Ager", "ager@chromium.org"),
+    Committer("Mahesh Kulkarni", ["mahesh.kulkarni@nokia.com", "maheshk@webkit.org"], "maheshk"),
+    Committer("Marcus Voltis Bulach", "bulach@chromium.org"),
+    Committer("Mario Sanchez Prada", ["msanchez@igalia.com", "mario@webkit.org"], "msanchez"),
+    Committer("Mark Lam", "mark.lam@apple.com", "mlam"),
+    Committer("Mary Wu", ["mary.wu@torchmobile.com.cn", "wwendy2007@gmail.com"], "marywu"),
+    Committer("Matt Delaney", "mdelaney@apple.com"),
+    Committer("Matt Lilek", ["mlilek@apple.com", "webkit@mattlilek.com", "pewtermoose@webkit.org"], "pewtermoose"),
+    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(u"Michael Br\u00fcning", ["michaelbruening@gmail.com", "michael.bruning@digia.com", "michael.bruning@nokia.com"], "mibrunin"),
+    Committer("Michael Nordman", "michaeln@google.com", "michaeln"),
+    Committer("Michelangelo De Simone", "michelangelo@webkit.org", "michelangelo"),
+    Committer("Mihnea Ovidenie", "mihnea@adobe.com", "mihnea"),
+    Committer("Mike Belshe", ["mbelshe@chromium.org", "mike@belshe.com"]),
+    Committer("Mike Fenton", ["mifenton@rim.com", "mike.fenton@torchmobile.com"], "mfenton"),
+    Committer("Mike Lawther", "mikelawther@chromium.org", "mikelawther"),
+    Committer("Mike Reed", "reed@google.com", "reed"),
+    Committer("Mike Thole", ["mthole@mikethole.com", "mthole@apple.com"]),
+    Committer("Mike West", ["mkwst@chromium.org", "mike@mikewest.org"], "mkwst"),
+    Committer("Mikhail Naganov", "mnaganov@chromium.org"),
+    Committer("Naoki Takano", ["honten@chromium.org", "takano.naoki@gmail.com"], "honten"),
+    Committer("Nat Duca", ["nduca@chromium.org", "nduca@google.com"], "nduca"),
+    Committer("Nayan Kumar K", ["nayankk@motorola.com", "nayankk@gmail.com"], "xc0ffee"),
+    Committer("Nico Weber", ["thakis@chromium.org", "thakis@google.com"], "thakis"),
+    Committer("Noel Gordon", ["noel.gordon@gmail.com", "noel@chromium.org", "noel@google.com"], "noel"),
+    Committer("Pam Greene", "pam@chromium.org", "pamg"),
+    Committer("Patrick Gansterer", ["paroga@paroga.com", "paroga@webkit.org"], "paroga"),
+    Committer("Pavel Podivilov", "podivilov@chromium.org", "podivilov"),
+    Committer("Peter Beverloo", ["peter@chromium.org", "peter@webkit.org", "beverloo@google.com"], "beverloo"),
+    Committer("Peter Kasting", ["pkasting@google.com", "pkasting@chromium.org"], "pkasting"),
+    Committer("Peter Varga", ["pvarga@webkit.org", "pvarga@inf.u-szeged.hu"], "stampho"),
+    Committer("Philip Rogers", ["pdr@google.com", "pdr@chromium.org"], "pdr"),
+    Committer("Pierre d'Herbemont", ["pdherbemont@free.fr", "pdherbemont@apple.com"], "pdherbemont"),
+    Committer("Pierre-Olivier Latour", "pol@apple.com", "pol"),
+    Committer("Pierre Rossi", "pierre.rossi@gmail.com", "elproxy"),
+    Committer("Pratik Solanki", "psolanki@apple.com", "psolanki"),
+    Committer("Qi Zhang", "qi.zhang02180@gmail.com", "qi"),
+    Committer("Rafael Antognolli", "antognolli@profusion.mobi", "antognolli"),
+    Committer("Rafael Brandao", "rafael.lobo@openbossa.org", "rafaelbrandao"),
+    Committer("Rafael Weinstein", "rafaelw@chromium.org", "rafaelw"),
+    Committer("Raphael Kubo da Costa", ["rakuco@webkit.org", "rakuco@FreeBSD.org", "raphael.kubo.da.costa@intel.com"], "rakuco"),
+    Committer("Ravi Kasibhatla", "ravi.kasibhatla@motorola.com", "kphanee"),
+    Committer("Renata Hodovan", "reni@webkit.org", "reni"),
+    Committer("Robert Hogan", ["robert@webkit.org", "robert@roberthogan.net", "lists@roberthogan.net"], "mwenge"),
+    Committer("Robert Kroeger", "rjkroege@chromium.org", "rjkroege"),
+    Committer("Roger Fong", "roger_fong@apple.com", "rfong"),
+    Committer("Roland Steiner", "rolandsteiner@chromium.org"),
+    Committer("Ryuan Choi", "ryuan.choi@samsung.com", "ryuan"),
+    Committer("Satish Sampath", "satish@chromium.org"),
+    Committer("Scott Violet", "sky@chromium.org", "sky"),
+    Committer("Sergio Villar Senin", ["svillar@igalia.com", "sergio@webkit.org"], "svillar"),
+    Committer("Shawn Singh", "shawnsingh@chromium.org", "shawnsingh"),
+    Committer("Shinya Kawanaka", "shinyak@chromium.org", "shinyak"),
+    Committer("Siddharth Mathur", "siddharth.mathur@nokia.com", "simathur"),
+    Committer("Simon Pena", "spena@igalia.com", "spenap"),
+    Committer("Stephen Chenney", "schenney@chromium.org", "schenney"),
+    Committer("Steve Lacey", "sjl@chromium.org", "stevela"),
+    Committer("Taiju Tsuiki", "tzik@chromium.org", "tzik"),
+    Committer("Takashi Sakamoto", "tasak@google.com", "tasak"),
+    Committer("Takashi Toyoshima", "toyoshim@chromium.org", "toyoshim"),
+    Committer("Terry Anderson", "tdanderson@chromium.org", "tdanderson"),
+    Committer("Thomas Sepez", "tsepez@chromium.org", "tsepez"),
+    Committer("Tom Hudson", ["tomhudson@google.com", "tomhudson@chromium.org"], "tomhudson"),
+    Committer("Tom Zakrajsek", "tomz@codeaurora.org", "tomz"),
+    Committer("Tommy Widenflycht", "tommyw@google.com", "tommyw"),
+    Committer("Trey Matteson", "trey@usa.net", "trey"),
+    Committer("Tristan O'Tierney", ["tristan@otierney.net", "tristan@apple.com"]),
+    Committer("Vangelis Kokkevis", "vangelis@chromium.org", "vangelis"),
+    Committer("Viatcheslav Ostapenko", ["ostap73@gmail.com", "v.ostapenko@samsung.com", "v.ostapenko@sisa.samsung.com"], "ostap"),
+    Committer("Victor Carbune", "victor@rosedu.org", "vcarbune"),
+    Committer("Victor Wang", "victorw@chromium.org", "victorw"),
+    Committer("Victoria Kirst", ["vrk@chromium.org", "vrk@google.com"], "vrk"),
+    Committer("Vincent Scheib", "scheib@chromium.org", "scheib"),
+    Committer("Vitaly Repeshko", "vitalyr@chromium.org"),
+    Committer("William Siegrist", "wsiegrist@apple.com", "wms"),
+    Committer("W. James MacLean", "wjmaclean@chromium.org", "seumas"),
+    Committer("Xianzhu Wang", ["wangxianzhu@chromium.org", "phnixwxz@gmail.com", "wangxianzhu@google.com"], "wangxianzhu"),
+    Committer("Xiaomei Ji", "xji@chromium.org", "xji"),
+    Committer("Yael Aharon", ["yael.aharon.m@gmail.com", "yael@webkit.org"], "yael"),
+    Committer("Yaar Schnitman", ["yaar@chromium.org", "yaar@google.com"]),
+    Committer("Yi Shen", ["yi.4.shen@nokia.com", "shenyi2006@gmail.com"]),
+    Committer("Yongjun Zhang", ["yongjun.zhang@nokia.com", "yongjun_zhang@apple.com"]),
+    Committer("Yoshifumi Inoue", "yosin@chromium.org", "yosin"),
+    Committer("Yuqiang Xian", "yuqiang.xian@intel.com"),
+    Committer("Yuzo Fujishima", "yuzo@google.com", "yuzo"),
+    Committer("Zalan Bujtas", ["zbujtas@gmail.com", "zalan.bujtas@nokia.com"], "zalan"),
+    Committer("Zeno Albisser", ["zeno@webkit.org", "zeno.albisser@nokia.com"], "zalbisser"),
+    Committer("Zhenyao Mo", "zmo@google.com", "zhenyao"),
+    Committer("Zoltan Horvath", ["zoltan@webkit.org", "hzoltan@inf.u-szeged.hu", "horvath.zoltan.6@stud.u-szeged.hu"], "zoltan"),
+    Committer(u"\u017dan Dober\u0161ek", "zandobersek@gmail.com", "zdobersek"),
+]
+
+
+# This is intended as a canonical, machine-readable list of all reviewers for
+# WebKit.  If your name is missing here and you are a reviewer, please add it.
+# No review needed.
+
+
+reviewers_list = [
+    Reviewer("Abhishek Arya", "inferno@chromium.org", "inferno-sec"),
+    Reviewer("Ada Chan", "adachan@apple.com", "chanada"),
+    Reviewer("Adam Barth", "abarth@webkit.org", "abarth"),
+    Reviewer("Adam Roben", ["aroben@webkit.org", "aroben@apple.com"], "aroben"),
+    Reviewer("Adam Treat", ["treat@kde.org", "treat@webkit.org", "atreat@rim.com"], "manyoso"),
+    Reviewer("Adele Peterson", "adele@apple.com", "adele"),
+    Reviewer("Adrienne Walker", ["enne@google.com", "enne@chromium.org"], "enne"),
+    Reviewer("Alejandro G. Castro", ["alex@igalia.com", "alex@webkit.org"], "alexg__"),
+    Reviewer("Alexander Pavlov", ["apavlov@chromium.org", "pavlov81@gmail.com"], "apavlov"),
+    Reviewer("Alexey Proskuryakov", ["ap@webkit.org", "ap@apple.com"], "ap"),
+    Reviewer("Alexis Menard", ["alexis@webkit.org", "menard@kde.org"], "darktears"),
+    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", ["kling@webkit.org", "awesomekling@apple.com", "andreas.kling@nokia.com"], "kling"),
+    Reviewer("Andy Estes", "aestes@apple.com", "estes"),
+    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.hidayat@gmail.com", "ariya@sencha.com", "ariya@webkit.org"], "ariya"),
+    Reviewer("Benjamin Poulain", ["benjamin@webkit.org", "benjamin.poulain@nokia.com", "ikipou@gmail.com"], "benjaminp"),
+    Reviewer("Beth Dakin", "bdakin@apple.com", "dethbakin"),
+    Reviewer("Brady Eidson", "beidson@apple.com", "bradee-oh"),
+    Reviewer("Brent Fulgham", "bfulgham@webkit.org", "bfulgham"),
+    Reviewer("Brian Weinstein", "bweinstein@apple.com", "bweinstein"),
+    Reviewer("Caio Marcelo de Oliveira Filho", ["cmarcelo@webkit.org", "caio.oliveira@openbossa.org"], "cmarcelo"),
+    Reviewer("Cameron Zwarich", ["zwarich@apple.com", "cwzwarich@apple.com", "cwzwarich@webkit.org"]),
+    Reviewer("Carlos Garcia Campos", ["cgarcia@igalia.com", "carlosgc@gnome.org", "carlosgc@webkit.org"], "KaL"),
+    Reviewer("Chang Shu", ["cshu@webkit.org", "c.shu@sisa.samsung.com"], "cshu"),
+    Reviewer("Chris Blumenberg", "cblu@apple.com", "cblu"),
+    Reviewer("Chris Marrin", "cmarrin@apple.com", "cmarrin"),
+    Reviewer("Chris Fleizach", "cfleizach@apple.com", "cfleizach"),
+    Reviewer("Chris Jerdonek", "cjerdonek@webkit.org", "cjerdonek"),
+    Reviewer("Chris Rogers", "crogers@google.com", "crogers"),
+    Reviewer(u"Csaba Osztrogon\u00e1c", "ossy@webkit.org", "ossy"),
+    Reviewer("Dan Bernstein", ["mitz@webkit.org", "mitz@apple.com"], "mitzpettel"),
+    Reviewer("Daniel Bates", ["dbates@webkit.org", "dbates@rim.com"], "dydz"),
+    Reviewer("Darin Adler", "darin@apple.com", "darin"),
+    Reviewer("Darin Fisher", ["fishd@chromium.org", "darin@chromium.org"], "fishd"),
+    Reviewer("David Harrison", "harrison@apple.com", "harrison"),
+    Reviewer("David Hyatt", "hyatt@apple.com", ["dhyatt", "hyatt"]),
+    Reviewer("David Kilzer", ["ddkilzer@webkit.org", "ddkilzer@apple.com"], "ddkilzer"),
+    Reviewer("David Levin", "levin@chromium.org", "dave_levin"),
+    Reviewer("Dean Jackson", "dino@apple.com", "dino"),
+    Reviewer("Dimitri Glazkov", "dglazkov@chromium.org", "dglazkov"),
+    Reviewer("Dirk Pranke", "dpranke@chromium.org", "dpranke"),
+    Reviewer("Dirk Schulze", "krit@webkit.org", "krit"),
+    Reviewer("Dmitry Titov", "dimich@chromium.org", "dimich"),
+    Reviewer("Don Melton", "gramps@apple.com", "gramps"),
+    Reviewer("Dumitru Daniliuc", "dumi@chromium.org", "dumi"),
+    Reviewer("Emil A Eklund", "eae@chromium.org", "eae"),
+    Reviewer("Enrica Casucci", "enrica@apple.com", "enrica"),
+    Reviewer("Eric Carlson", "eric.carlson@apple.com", "eric_carlson"),
+    Reviewer("Eric Seidel", "eric@webkit.org", "eseidel"),
+    Reviewer("Filip Pizlo", "fpizlo@apple.com", "pizlo"),
+    Reviewer("Gavin Barraclough", "barraclough@apple.com", "gbarra"),
+    Reviewer("Geoffrey Garen", "ggaren@apple.com", "ggaren"),
+    Reviewer("George Staikos", ["staikos@kde.org", "staikos@webkit.org"]),
+    Reviewer("Gustavo Noronha Silva", ["gns@gnome.org", "kov@webkit.org", "gustavo.noronha@collabora.co.uk", "gustavo.noronha@collabora.com"], "kov"),
+    Reviewer("Gyuyoung Kim", ["gyuyoung.kim@samsung.com", "gyuyoung.kim@webkit.org"], "gyuyoung"),
+    Reviewer("Hajime Morita", ["morrita@google.com", "morrita@chromium.org"], "morrita"),
+    Reviewer("Holger Freyther", ["zecke@selfish.org", "zecke@webkit.org"], "zecke"),
+    Reviewer("James Robinson", ["jamesr@chromium.org", "jamesr@google.com"], "jamesr"),
+    Reviewer("Jan Alonzo", ["jmalonzo@gmail.com", "jmalonzo@webkit.org"], "janm"),
+    Reviewer("Jeremy Orlow", ["jorlow@webkit.org", "jorlow@chromium.org"], "jorlow"),
+    Reviewer("Jessie Berlin", ["jberlin@webkit.org", "jberlin@apple.com"], "jessieberlin"),
+    Reviewer("Jian Li", "jianli@chromium.org", "jianli"),
+    Reviewer("Jocelyn Turcotte", ["jocelyn.turcotte@digia.com", "jocelyn.turcotte@nokia.com"], "jturcotte"),
+    Reviewer("Jochen Eisinger", "jochen@chromium.org", "jochen__"),
+    Reviewer("John Sullivan", "sullivan@apple.com", "sullivan"),
+    Reviewer("Jon Honeycutt", "jhoneycutt@apple.com", "jhoneycutt"),
+    Reviewer("Joseph Pecoraro", ["joepeck@webkit.org", "pecoraro@apple.com"], "JoePeck"),
+    Reviewer("Julien Chaffraix", ["jchaffraix@webkit.org", "julien.chaffraix@gmail.com", "jchaffraix@google.com", "jchaffraix@codeaurora.org"], "jchaffraix"),
+    Reviewer("Justin Garcia", "justin.garcia@apple.com", "justing"),
+    Reviewer("Ken Kocienda", "kocienda@apple.com"),
+    Reviewer("Kenneth Rohde Christiansen", ["kenneth@webkit.org", "kenneth.r.christiansen@intel.com", "kenneth.christiansen@gmail.com"], ["kenneth_", "kenneth", "kenne"]),
+    Reviewer("Kenneth Russell", ["kbr@google.com", "kbr@chromium.org"], ["kbr_google", "kbrgg"]),
+    Reviewer("Kent Tamura", ["tkent@chromium.org", "tkent@google.com"], "tkent"),
+    Reviewer("Kentaro Hara", ["haraken@chromium.org"], "haraken"),
+    Reviewer("Kevin Decker", "kdecker@apple.com", "superkevin"),
+    Reviewer("Kevin McCullough", "kmccullough@apple.com", "maculloch"),
+    Reviewer("Kevin Ollivier", ["kevino@theolliviers.com", "kevino@webkit.org"], "kollivier"),
+    Reviewer("Lars Knoll", ["lars@trolltech.com", "lars@kde.org", "lars.knoll@nokia.com"], "lars"),
+    Reviewer("Laszlo Gombos", ["laszlo.gombos@webkit.org", "l.gombos@samsung.com", "laszlo.1.gombos@nokia.com"], "lgombos"),
+    Reviewer("Levi Weintraub", ["leviw@chromium.org", "leviw@google.com", "lweintraub@apple.com"], "leviw"),
+    Reviewer("Luiz Agostini", ["luiz@webkit.org", "luiz.agostini@openbossa.org"], "lca"),
+    Reviewer("Maciej Stachowiak", "mjs@apple.com", "othermaciej"),
+    Reviewer("Mark Hahnenberg", "mhahnenberg@apple.com", "mhahnenberg"),
+    Reviewer("Mark Rowe", "mrowe@apple.com", "bdash"),
+    Reviewer("Martin Robinson", ["mrobinson@webkit.org", "mrobinson@igalia.com", "martin.james.robinson@gmail.com"], "mrobinson"),
+    Reviewer("Michael Saboff", "msaboff@apple.com", "msaboff"),
+    Reviewer("Mihai Parparita", "mihaip@chromium.org", "mihaip"),
+    Reviewer("Nate Chapin", "japhet@chromium.org", ["japhet", "natechapin"]),
+    Reviewer("Nikolas Zimmermann", ["zimmermann@kde.org", "zimmermann@physik.rwth-aachen.de", "zimmermann@webkit.org", "nzimmermann@rim.com"], "wildfox"),
+    Reviewer("Noam Rosenthal", "noam.rosenthal@nokia.com", "noamr"),
+    Reviewer("Ojan Vafai", "ojan@chromium.org", "ojan"),
+    Reviewer("Oliver Hunt", "oliver@apple.com", "olliej"),
+    Reviewer("Pavel Feldman", ["pfeldman@chromium.org", "pfeldman@google.com"], "pfeldman"),
+    Reviewer("Philippe Normand", ["pnormand@igalia.com", "philn@webkit.org", "philn@igalia.com"], ["philn-tp", "pnormand"]),
+    Reviewer("Richard Williamson", "rjw@apple.com", "rjw"),
+    Reviewer("Rob Buis", ["rwlbuis@gmail.com", "rwlbuis@webkit.org", "rbuis@rim.com"], "rwlbuis"),
+    Reviewer("Ryosuke Niwa", "rniwa@webkit.org", "rniwa"),
+    Reviewer("Sam Weinig", ["sam@webkit.org", "weinig@apple.com"], "weinig"),
+    Reviewer("Shinichiro Hamaji", "hamaji@chromium.org", "hamaji"),
+    Reviewer("Simon Fraser", "simon.fraser@apple.com", "smfr"),
+    Reviewer("Simon Hausmann", ["hausmann@webkit.org", "hausmann@kde.org", "simon.hausmann@digia.com"], "tronical"),
+    Reviewer("Stephanie Lewis", "slewis@apple.com", "sundiamonde"),
+    Reviewer("Stephen White", "senorblanco@chromium.org", "senorblanco"),
+    Reviewer("Steve Block", "steveblock@google.com", "steveblock"),
+    Reviewer("Steve Falkenburg", "sfalken@apple.com", "sfalken"),
+    Reviewer("Tim Omernick", "timo@apple.com"),
+    Reviewer("Timothy Hatcher", ["timothy@apple.com", "timothy@hatcher.name"], "xenon"),
+    Reviewer("Tim Horton", "timothy_horton@apple.com", "thorton"),
+    Reviewer("Tony Chang", "tony@chromium.org", "tony^work"),
+    Reviewer("Tony Gentilcore", "tonyg@chromium.org", "tonyg-cr"),
+    Reviewer(u"Tor Arne Vestb\u00f8", ["vestbo@webkit.org", "tor.arne.vestbo@nokia.com"], "torarne"),
+    Reviewer("Vicki Murley", "vicki@apple.com"),
+    Reviewer("Vsevolod Vlasov", "vsevik@chromium.org", "vsevik"),
+    Reviewer("Xan Lopez", ["xan.lopez@gmail.com", "xan@gnome.org", "xan@webkit.org", "xlopez@igalia.com"], "xan"),
+    Reviewer("Yong Li", ["yoli@rim.com", "yong.li.webkit@gmail.com"], "yoli"),
+    Reviewer("Yury Semikhatsky", "yurys@chromium.org", "yurys"),
+    Reviewer("Yuta Kitamura", "yutak@chromium.org", "yutak"),
+    Reviewer("Zack Rusin", "zack@kde.org", "zackr"),
+    Reviewer("Zoltan Herczeg", ["zherczeg@webkit.org", "zherczeg@inf.u-szeged.hu"], "zherczeg"),
+]
+
+class CommitterList(object):
+
+    # Committers and reviewers are passed in to allow easy testing
+    def __init__(self,
+                 committers=committers_unable_to_review,
+                 reviewers=reviewers_list,
+                 contributors=contributors_who_are_not_committers,
+                 watchers=watchers_who_are_not_contributors):
+        self._accounts = watchers + contributors + committers + reviewers
+        self._contributors = contributors + committers + reviewers
+        self._committers = committers + reviewers
+        self._reviewers = reviewers
+        self._contributors_by_name = {}
+        self._accounts_by_email = {}
+        self._accounts_by_login = {}
+
+    def accounts(self):
+        return self._accounts
+
+    def contributors(self):
+        return self._contributors
+
+    def committers(self):
+        return self._committers
+
+    def reviewers(self):
+        return self._reviewers
+
+    def _name_to_contributor_map(self):
+        if not len(self._contributors_by_name):
+            for contributor in self._contributors:
+                assert(contributor.full_name)
+                assert(contributor.full_name.lower() not in self._contributors_by_name)  # We should never have duplicate names.
+                self._contributors_by_name[contributor.full_name.lower()] = contributor
+        return self._contributors_by_name
+
+    def _email_to_account_map(self):
+        if not len(self._accounts_by_email):
+            for account in self._accounts:
+                for email in account.emails:
+                    assert(email not in self._accounts_by_email)  # We should never have duplicate emails.
+                    self._accounts_by_email[email] = account
+        return self._accounts_by_email
+
+    def _login_to_account_map(self):
+        if not len(self._accounts_by_login):
+            for account in self._accounts:
+                if account.emails:
+                    login = account.bugzilla_email()
+                    assert(login not in self._accounts_by_login)  # We should never have duplicate emails.
+                    self._accounts_by_login[login] = account
+        return self._accounts_by_login
+
+    def _contributor_only(self, record):
+        if record and not record.is_contributor:
+            return None
+        return record
+
+    def _committer_only(self, record):
+        if record and not record.can_commit:
+            return None
+        return record
+
+    def _reviewer_only(self, record):
+        if record and not record.can_review:
+            return None
+        return record
+
+    def committer_by_name(self, name):
+        return self._committer_only(self.contributor_by_name(name))
+
+    def contributor_by_irc_nickname(self, irc_nickname):
+        for contributor in self.contributors():
+            # FIXME: This should do case-insensitive comparison or assert that all IRC nicknames are in lowercase
+            if contributor.irc_nicknames and irc_nickname in contributor.irc_nicknames:
+                return contributor
+        return None
+
+    def contributors_by_search_string(self, string):
+        return filter(lambda contributor: contributor.contains_string(string), self.contributors())
+
+    def contributors_by_email_username(self, string):
+        string = string + '@'
+        result = []
+        for contributor in self.contributors():
+            for email in contributor.emails:
+                if email.startswith(string):
+                    result.append(contributor)
+                    break
+        return result
+
+    def _contributor_name_shorthands(self, contributor):
+        if ' ' not in contributor.full_name:
+            return []
+        split_fullname = contributor.full_name.split()
+        first_name = split_fullname[0]
+        last_name = split_fullname[-1]
+        return first_name, last_name, first_name + last_name[0], first_name + ' ' + last_name[0]
+
+    def _tokenize_contributor_name(self, contributor):
+        full_name_in_lowercase = contributor.full_name.lower()
+        tokens = [full_name_in_lowercase] + full_name_in_lowercase.split()
+        if contributor.irc_nicknames:
+            return tokens + [nickname.lower() for nickname in contributor.irc_nicknames if len(nickname) > 5]
+        return tokens
+
+    def contributors_by_fuzzy_match(self, string):
+        string_in_lowercase = string.lower()
+
+        # 1. Exact match for fullname, email and irc_nicknames
+        account = self.contributor_by_name(string_in_lowercase) or self.account_by_email(string_in_lowercase) or self.contributor_by_irc_nickname(string_in_lowercase)
+        if account:
+            return [account], 0
+
+        # 2. Exact match for email username (before @)
+        accounts = self.contributors_by_email_username(string_in_lowercase)
+        if accounts and len(accounts) == 1:
+            return accounts, 0
+
+        # 3. Exact match for first name, last name, and first name + initial combinations such as "Dan B" and "Tim H"
+        accounts = [contributor for contributor in self.contributors() if string in self._contributor_name_shorthands(contributor)]
+        if accounts and len(accounts) == 1:
+            return accounts, 0
+
+        # 4. Finally, fuzzy-match using edit-distance
+        string = string_in_lowercase
+        contributorWithMinDistance = []
+        minDistance = len(string) / 2 - 1
+        for contributor in self.contributors():
+            tokens = self._tokenize_contributor_name(contributor)
+            editdistances = [edit_distance(token, string) for token in tokens if abs(len(token) - len(string)) <= minDistance]
+            if not editdistances:
+                continue
+            distance = min(editdistances)
+            if distance == minDistance:
+                contributorWithMinDistance.append(contributor)
+            elif distance < minDistance:
+                contributorWithMinDistance = [contributor]
+                minDistance = distance
+        if not len(contributorWithMinDistance):
+            return [], len(string)
+        return contributorWithMinDistance, minDistance
+
+    def account_by_login(self, login):
+        return self._login_to_account_map().get(login.lower()) if login else None
+
+    def account_by_email(self, email):
+        return self._email_to_account_map().get(email.lower()) if email else None
+
+    def contributor_by_name(self, name):
+        return self._name_to_contributor_map().get(name.lower()) if name else None
+
+    def contributor_by_email(self, email):
+        return self._contributor_only(self.account_by_email(email))
+
+    def committer_by_email(self, email):
+        return self._committer_only(self.account_by_email(email))
+
+    def reviewer_by_email(self, email):
+        return self._reviewer_only(self.account_by_email(email))
diff --git a/Tools/Scripts/webkitpy/common/config/committers_unittest.py b/Tools/Scripts/webkitpy/common/config/committers_unittest.py
new file mode 100644
index 0000000..1c8c86a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/config/committers_unittest.py
@@ -0,0 +1,368 @@
+# Copyright (C) 2009 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.config.committers import Account, CommitterList, Contributor, Committer, Reviewer
+
+class CommittersTest(unittest.TestCase):
+    def test_committer_lookup(self):
+        account = Account('Test Zero', ['zero@test.com', 'zero@gmail.com'], 'zero')
+        committer = Committer('Test One', 'one@test.com', 'one')
+        reviewer = Reviewer('Test Two', ['two@test.com', 'Two@rad.com', 'so_two@gmail.com'])
+        contributor = Contributor('Test Three', ['Three@test.com'], 'three')
+        contributor_with_two_nicknames = Contributor('Other Four', ['otherfour@webkit.org', 'otherfour@webkit2.org'], ['four', 'otherfour'])
+        contributor_with_same_email_username = Contributor('Yet Another Four', ['otherfour@webkit.com'], ['yetanotherfour'])
+        committer_list = CommitterList(watchers=[account], committers=[committer], reviewers=[reviewer],
+            contributors=[contributor, contributor_with_two_nicknames, contributor_with_same_email_username])
+
+        # Test valid committer, reviewer and contributor lookup
+        self.assertEqual(committer_list.account_by_email('zero@test.com'), account)
+        self.assertEqual(committer_list.committer_by_email('one@test.com'), committer)
+        self.assertEqual(committer_list.reviewer_by_email('two@test.com'), reviewer)
+        self.assertEqual(committer_list.committer_by_email('two@test.com'), reviewer)
+        self.assertEqual(committer_list.committer_by_email('two@rad.com'), reviewer)
+        self.assertEqual(committer_list.reviewer_by_email('so_two@gmail.com'), reviewer)
+        self.assertEqual(committer_list.contributor_by_email('three@test.com'), contributor)
+
+        # Test valid committer, reviewer and contributor lookup
+        self.assertEqual(committer_list.committer_by_name("Test One"), committer)
+        self.assertEqual(committer_list.committer_by_name("Test Two"), reviewer)
+        self.assertEqual(committer_list.committer_by_name("Test Three"), None)
+        self.assertEqual(committer_list.contributor_by_name("Test Three"), contributor)
+        self.assertEqual(committer_list.contributor_by_name("test one"), committer)
+        self.assertEqual(committer_list.contributor_by_name("test two"), reviewer)
+        self.assertEqual(committer_list.contributor_by_name("test three"), contributor)
+
+        # Test that the first email is assumed to be the Bugzilla email address (for now)
+        self.assertEqual(committer_list.committer_by_email('two@rad.com').bugzilla_email(), 'two@test.com')
+
+        # Test lookup by login email address
+        self.assertEqual(committer_list.account_by_login('zero@test.com'), account)
+        self.assertEqual(committer_list.account_by_login('zero@gmail.com'), None)
+        self.assertEqual(committer_list.account_by_login('one@test.com'), committer)
+        self.assertEqual(committer_list.account_by_login('two@test.com'), reviewer)
+        self.assertEqual(committer_list.account_by_login('Two@rad.com'), None)
+        self.assertEqual(committer_list.account_by_login('so_two@gmail.com'), None)
+
+        # Test that a known committer is not returned during reviewer lookup
+        self.assertEqual(committer_list.reviewer_by_email('one@test.com'), None)
+        self.assertEqual(committer_list.reviewer_by_email('three@test.com'), None)
+        # and likewise that a known contributor is not returned for committer lookup.
+        self.assertEqual(committer_list.committer_by_email('three@test.com'), None)
+
+        # Test that unknown email address fail both committer and reviewer lookup
+        self.assertEqual(committer_list.committer_by_email('bar@bar.com'), None)
+        self.assertEqual(committer_list.reviewer_by_email('bar@bar.com'), None)
+
+        # Test that emails returns a list.
+        self.assertEqual(committer.emails, ['one@test.com'])
+
+        self.assertEqual(committer.irc_nicknames, ['one'])
+        self.assertEqual(committer_list.contributor_by_irc_nickname('one'), committer)
+        self.assertEqual(committer_list.contributor_by_irc_nickname('three'), contributor)
+        self.assertEqual(committer_list.contributor_by_irc_nickname('four'), contributor_with_two_nicknames)
+        self.assertEqual(committer_list.contributor_by_irc_nickname('otherfour'), contributor_with_two_nicknames)
+
+        # Test that the lists returned are are we expect them.
+        self.assertEqual(committer_list.contributors(), [contributor, contributor_with_two_nicknames, contributor_with_same_email_username, committer, reviewer])
+        self.assertEqual(committer_list.committers(), [committer, reviewer])
+        self.assertEqual(committer_list.reviewers(), [reviewer])
+
+        self.assertEqual(committer_list.contributors_by_search_string('test'), [contributor, committer, reviewer])
+        self.assertEqual(committer_list.contributors_by_search_string('rad'), [reviewer])
+        self.assertEqual(committer_list.contributors_by_search_string('Two'), [reviewer])
+
+        self.assertEqual(committer_list.contributors_by_email_username("one"), [committer])
+        self.assertEqual(committer_list.contributors_by_email_username("four"), [])
+        self.assertEqual(committer_list.contributors_by_email_username("otherfour"), [contributor_with_two_nicknames, contributor_with_same_email_username])
+
+    def _assert_fuzz_match(self, text, name_of_expected_contributor, expected_distance):
+        committers = CommitterList()
+        contributors, distance = committers.contributors_by_fuzzy_match(text)
+        if type(name_of_expected_contributor) is list:
+            expected_names = name_of_expected_contributor
+        else:
+            expected_names = [name_of_expected_contributor] if name_of_expected_contributor else []
+        self.assertEqual(([contributor.full_name for contributor in contributors], distance), (expected_names, expected_distance))
+
+    # Basic testing of the edit distance matching ...
+    def test_contributors_by_fuzzy_match(self):
+        self._assert_fuzz_match('Geoff Garen', 'Geoffrey Garen', 3)
+        self._assert_fuzz_match('Kenneth Christiansen', 'Kenneth Rohde Christiansen', 6)
+        self._assert_fuzz_match('Sam', 'Sam Weinig', 0)
+        self._assert_fuzz_match('me', None, 2)
+
+    # The remaining tests test that certain names are resolved in a specific way.
+    # We break this up into multiple tests so that each is faster and they can
+    # be run in parallel. Unfortunately each test scans the entire committers list,
+    # so these are inherently slow (see https://bugs.webkit.org/show_bug.cgi?id=79179).
+    #
+    # Commented out lines are test cases imported from the bug 26533 yet to pass.
+
+    def integration_test_contributors__none(self):
+        self._assert_fuzz_match('myself', None, 6)
+        self._assert_fuzz_match('others', None, 6)
+        self._assert_fuzz_match('BUILD FIX', None, 9)
+
+    def integration_test_contributors__none_2(self):
+        self._assert_fuzz_match('but Dan Bernstein also reviewed', None, 31)
+        self._assert_fuzz_match('asked thoughtful questions', None, 26)
+        self._assert_fuzz_match('build fix of mac', None, 16)
+
+    def integration_test_contributors__none_3(self):
+        self._assert_fuzz_match('a spell checker', None, 15)
+        self._assert_fuzz_match('nobody, build fix', None, 17)
+        self._assert_fuzz_match('NOBODY (chromium build fix)', None, 27)
+
+    def integration_test_contributors_ada_chan(self):
+        self._assert_fuzz_match('Ada', 'Ada Chan', 0)
+
+    def integration_test_contributors_adele_peterson(self):
+        self._assert_fuzz_match('adele', 'Adele Peterson', 0)
+
+    def integration_test_contributors_adele_peterson(self):
+        # self._assert_fuzz_match('Adam', 'Adam Roben', 0)
+        self._assert_fuzz_match('aroben', 'Adam Roben', 0)
+
+    def integration_test_contributors_alexey_proskuryakov(self):
+        # self._assert_fuzz_match('Alexey', 'Alexey Proskuryakov', 0)
+        self._assert_fuzz_match('ap', 'Alexey Proskuryakov', 0)
+        self._assert_fuzz_match('Alexey P', 'Alexey Proskuryakov', 0)
+
+    def integration_test_contributors_alice_liu(self):
+        # self._assert_fuzz_match('Alice', 'Alice Liu', 0)
+        self._assert_fuzz_match('aliu', 'Alice Liu', 0)
+        self._assert_fuzz_match('Liu', 'Alice Liu', 0)
+
+    def integration_test_contributors_alp_toker(self):
+        self._assert_fuzz_match('Alp', 'Alp Toker', 0)
+
+    def integration_test_contributors_anders_carlsson(self):
+        self._assert_fuzz_match('Anders', 'Anders Carlsson', 0)
+        self._assert_fuzz_match('andersca', 'Anders Carlsson', 0)
+        self._assert_fuzz_match('anders', 'Anders Carlsson', 0)
+        self._assert_fuzz_match('Andersca', 'Anders Carlsson', 0)
+
+    def integration_test_contributors_antti_koivisto(self):
+        self._assert_fuzz_match('Antti "printf" Koivisto', 'Antti Koivisto', 9)
+        self._assert_fuzz_match('Antti', 'Antti Koivisto', 0)
+
+    def integration_test_contributors_beth_dakin(self):
+        self._assert_fuzz_match('Beth', 'Beth Dakin', 0)
+        self._assert_fuzz_match('beth', 'Beth Dakin', 0)
+        self._assert_fuzz_match('bdakin', 'Beth Dakin', 0)
+
+    def integration_test_contributors_brady_eidson(self):
+        self._assert_fuzz_match('Brady', 'Brady Eidson', 0)
+        self._assert_fuzz_match('bradee-oh', 'Brady Eidson', 0)
+        self._assert_fuzz_match('Brady', 'Brady Eidson', 0)
+
+    def integration_test_contributors_cameron_zwarich(self):
+        pass  # self._assert_fuzz_match('Cameron', 'Cameron Zwarich', 0)
+        # self._assert_fuzz_match('cpst', 'Cameron Zwarich', 1)
+
+    def integration_test_contributors_chris_blumenberg(self):
+        # self._assert_fuzz_match('Chris', 'Chris Blumenberg', 0)
+        self._assert_fuzz_match('cblu', 'Chris Blumenberg', 0)
+
+    def integration_test_contributors_dan_bernstein(self):
+        self._assert_fuzz_match('Dan', ['Dan Winship', 'Dan Bernstein'], 0)
+        self._assert_fuzz_match('Dan B', 'Dan Bernstein', 0)
+        # self._assert_fuzz_match('mitz', 'Dan Bernstein', 0)
+        self._assert_fuzz_match('Mitz Pettel', 'Dan Bernstein', 1)
+        self._assert_fuzz_match('Mitzpettel', 'Dan Bernstein', 0)
+        self._assert_fuzz_match('Mitz Pettel RTL', 'Dan Bernstein', 5)
+
+    def integration_test_contributors_dan_bernstein_2(self):
+        self._assert_fuzz_match('Teh Mitzpettel', 'Dan Bernstein', 4)
+        # self._assert_fuzz_match('The Mitz', 'Dan Bernstein', 0)
+        self._assert_fuzz_match('Dr Dan Bernstein', 'Dan Bernstein', 3)
+
+    def integration_test_contributors_darin_adler(self):
+        self._assert_fuzz_match('Darin Adler\'', 'Darin Adler', 1)
+        self._assert_fuzz_match('Darin', 'Darin Adler', 0)  # Thankfully "Fisher" is longer than "Adler"
+        self._assert_fuzz_match('darin', 'Darin Adler', 0)
+
+    def integration_test_contributors_david_harrison(self):
+        self._assert_fuzz_match('Dave Harrison', 'David Harrison', 2)
+        self._assert_fuzz_match('harrison', 'David Harrison', 0)
+        self._assert_fuzz_match('Dr. Harrison', 'David Harrison', 4)
+
+    def integration_test_contributors_david_harrison_2(self):
+        self._assert_fuzz_match('Dave Harrson', 'David Harrison', 3)
+        self._assert_fuzz_match('Dave Harrsion', 'David Harrison', 4)  # Damerau-Levenshtein distance is 3
+
+    def integration_test_contributors_david_hyatt(self):
+        self._assert_fuzz_match('Dave Hyatt', 'David Hyatt', 2)
+        self._assert_fuzz_match('Daddy Hyatt', 'David Hyatt', 3)
+        # self._assert_fuzz_match('Dave', 'David Hyatt', 0)  # 'Dave' could mean harrison.
+        self._assert_fuzz_match('hyatt', 'David Hyatt', 0)
+        # self._assert_fuzz_match('Haytt', 'David Hyatt', 0)  # Works if we had implemented Damerau-Levenshtein distance!
+
+    def integration_test_contributors_david_kilzer(self):
+        self._assert_fuzz_match('Dave Kilzer', 'David Kilzer', 2)
+        self._assert_fuzz_match('David D. Kilzer', 'David Kilzer', 3)
+        self._assert_fuzz_match('ddkilzer', 'David Kilzer', 0)
+
+    def integration_test_contributors_don_melton(self):
+        self._assert_fuzz_match('Don', 'Don Melton', 0)
+        self._assert_fuzz_match('Gramps', 'Don Melton', 0)
+
+    def integration_test_contributors_eric_seidel(self):
+        # self._assert_fuzz_match('eric', 'Eric Seidel', 0)
+        self._assert_fuzz_match('Eric S', 'Eric Seidel', 0)
+        # self._assert_fuzz_match('MacDome', 'Eric Seidel', 0)
+        self._assert_fuzz_match('eseidel', 'Eric Seidel', 0)
+
+    def integration_test_contributors_geoffrey_garen(self):
+        # self._assert_fuzz_match('Geof', 'Geoffrey Garen', 4)
+        # self._assert_fuzz_match('Geoff', 'Geoffrey Garen', 3)
+        self._assert_fuzz_match('Geoff Garen', 'Geoffrey Garen', 3)
+        self._assert_fuzz_match('ggaren', 'Geoffrey Garen', 0)
+        # self._assert_fuzz_match('geoff', 'Geoffrey Garen', 0)
+        self._assert_fuzz_match('Geoffrey', 'Geoffrey Garen', 0)
+        self._assert_fuzz_match('GGaren', 'Geoffrey Garen', 0)
+
+    def integration_test_contributors_greg_bolsinga(self):
+        pass  # self._assert_fuzz_match('Greg', 'Greg Bolsinga', 0)
+
+    def integration_test_contributors_holger_freyther(self):
+        self._assert_fuzz_match('Holger', 'Holger Freyther', 0)
+        self._assert_fuzz_match('Holger Hans Peter Freyther', 'Holger Freyther', 11)
+
+    def integration_test_contributors_jon_sullivan(self):
+        # self._assert_fuzz_match('john', 'John Sullivan', 0)
+        self._assert_fuzz_match('sullivan', 'John Sullivan', 0)
+
+    def integration_test_contributors_jon_honeycutt(self):
+        self._assert_fuzz_match('John Honeycutt', 'Jon Honeycutt', 1)
+        # self._assert_fuzz_match('Jon', 'Jon Honeycutt', 0)
+
+    def integration_test_contributors_jon_honeycutt(self):
+        # self._assert_fuzz_match('justin', 'Justin Garcia', 0)
+        self._assert_fuzz_match('justing', 'Justin Garcia', 0)
+
+    def integration_test_contributors_joseph_pecoraro(self):
+        self._assert_fuzz_match('Joe Pecoraro', 'Joseph Pecoraro', 3)
+
+    def integration_test_contributors_ken_kocienda(self):
+        self._assert_fuzz_match('ken', 'Ken Kocienda', 0)
+        self._assert_fuzz_match('kocienda', 'Ken Kocienda', 0)
+
+    def integration_test_contributors_kenneth_russell(self):
+        self._assert_fuzz_match('Ken Russell', 'Kenneth Russell', 4)
+
+    def integration_test_contributors_kevin_decker(self):
+        self._assert_fuzz_match('kdecker', 'Kevin Decker', 0)
+
+    def integration_test_contributors_kevin_mccullough(self):
+        self._assert_fuzz_match('Kevin M', 'Kevin McCullough', 0)
+        self._assert_fuzz_match('Kevin McCulough', 'Kevin McCullough', 1)
+        self._assert_fuzz_match('mccullough', 'Kevin McCullough', 0)
+
+    def integration_test_contributors_lars_knoll(self):
+        self._assert_fuzz_match('lars', 'Lars Knoll', 0)
+
+    def integration_test_contributors_lars_weintraub(self):
+        self._assert_fuzz_match('levi', 'Levi Weintraub', 0)
+
+    def integration_test_contributors_maciej_stachowiak(self):
+        self._assert_fuzz_match('Maciej', 'Maciej Stachowiak', 0)
+        # self._assert_fuzz_match('mjs', 'Maciej Stachowiak', 0)
+        self._assert_fuzz_match('Maciej S', 'Maciej Stachowiak', 0)
+
+    def integration_test_contributors_mark_rowe(self):
+        # self._assert_fuzz_match('Mark', 'Mark Rowe', 0)
+        self._assert_fuzz_match('bdash', 'Mark Rowe', 0)
+        self._assert_fuzz_match('mrowe', 'Mark Rowe', 0)
+        # self._assert_fuzz_match('Brian Dash', 'Mark Rowe', 0)
+
+    def integration_test_contributors_nikolas_zimmermann(self):
+        # self._assert_fuzz_match('Niko', 'Nikolas Zimmermann', 1)
+        self._assert_fuzz_match('Niko Zimmermann', 'Nikolas Zimmermann', 3)
+        self._assert_fuzz_match('Nikolas', 'Nikolas Zimmermann', 0)
+
+    def integration_test_contributors_oliver_hunt(self):
+        #  self._assert_fuzz_match('Oliver', 'Oliver Hunt', 0)
+        self._assert_fuzz_match('Ollie', 'Oliver Hunt', 1)
+        self._assert_fuzz_match('Olliej', 'Oliver Hunt', 0)
+        self._assert_fuzz_match('Olliej Hunt', 'Oliver Hunt', 3)
+        self._assert_fuzz_match('olliej', 'Oliver Hunt', 0)
+        self._assert_fuzz_match('ollie', 'Oliver Hunt', 1)
+        self._assert_fuzz_match('ollliej', 'Oliver Hunt', 1)
+
+    def integration_test_contributors_oliver_hunt(self):
+        self._assert_fuzz_match('Richard', 'Richard Williamson', 0)
+        self._assert_fuzz_match('rjw', 'Richard Williamson', 0)
+
+    def integration_test_contributors_oliver_hunt(self):
+        self._assert_fuzz_match('Rob', 'Rob Buis', 0)
+        self._assert_fuzz_match('rwlbuis', 'Rob Buis', 0)
+
+    def integration_test_contributors_rniwa(self):
+        self._assert_fuzz_match('rniwa@webkit.org', 'Ryosuke Niwa', 0)
+
+    def disabled_integration_test_contributors_simon_fraser(self):
+        pass  # self._assert_fuzz_match('Simon', 'Simon Fraser', 0)
+
+    def integration_test_contributors_steve_falkenburg(self):
+        self._assert_fuzz_match('Sfalken', 'Steve Falkenburg', 0)
+        # self._assert_fuzz_match('Steve', 'Steve Falkenburg', 0)
+
+    def integration_test_contributors_sam_weinig(self):
+        self._assert_fuzz_match('Sam', 'Sam Weinig', 0)
+        # self._assert_fuzz_match('Weinig Sam', 'weinig', 0)
+        self._assert_fuzz_match('Weinig', 'Sam Weinig', 0)
+        self._assert_fuzz_match('Sam W', 'Sam Weinig', 0)
+        self._assert_fuzz_match('Sammy Weinig', 'Sam Weinig', 2)
+
+    def integration_test_contributors_tim_omernick(self):
+        # self._assert_fuzz_match('timo', 'Tim Omernick', 0)
+        self._assert_fuzz_match('TimO', 'Tim Omernick', 0)
+        # self._assert_fuzz_match('Timo O', 'Tim Omernick', 0)
+        # self._assert_fuzz_match('Tim O.', 'Tim Omernick', 0)
+        self._assert_fuzz_match('Tim O', 'Tim Omernick', 0)
+
+    def integration_test_contributors_timothy_hatcher(self):
+        # self._assert_fuzz_match('Tim', 'Timothy Hatcher', 0)
+        # self._assert_fuzz_match('Tim H', 'Timothy Hatcher', 0)
+        self._assert_fuzz_match('Tim Hatcher', 'Timothy Hatcher', 4)
+        self._assert_fuzz_match('Tim Hatcheri', 'Timothy Hatcher', 5)
+        self._assert_fuzz_match('timothy', 'Timothy Hatcher', 0)
+        self._assert_fuzz_match('thatcher', 'Timothy Hatcher', 1)
+        self._assert_fuzz_match('xenon', 'Timothy Hatcher', 0)
+        self._assert_fuzz_match('Hatcher', 'Timothy Hatcher', 0)
+        # self._assert_fuzz_match('TimH', 'Timothy Hatcher', 0)
+
+    def integration_test_contributors_tor_arne_vestbo(self):
+        self._assert_fuzz_match('Tor Arne', u"Tor Arne Vestb\u00f8", 1)  # Matches IRC nickname
+
+    def integration_test_contributors_vicki_murley(self):
+        self._assert_fuzz_match('Vicki', u"Vicki Murley", 0)
+
+    def integration_test_contributors_zack_rusin(self):
+        self._assert_fuzz_match('Zack', 'Zack Rusin', 0)
diff --git a/Tools/Scripts/webkitpy/common/config/committervalidator.py b/Tools/Scripts/webkitpy/common/config/committervalidator.py
new file mode 100644
index 0000000..6cec3da
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/config/committervalidator.py
@@ -0,0 +1,95 @@
+# Copyright (c) 2009 Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+# Copyright (c) 2010 Research In Motion Limited. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.config import committers, urls
+
+
+class CommitterValidator(object):
+    def __init__(self, host):
+        self.host = host
+
+    def _committers_py_path(self):
+        # extension can sometimes be .pyc, we always want .py
+        committers_path = self.host.filesystem.path_to_module(committers.__name__)
+        (path, extension) = self.host.filesystem.splitext(committers_path)
+        path = self.host.filesystem.relpath(path, self.host.scm().checkout_root)
+        return ".".join([path, "py"])
+
+    def _flag_permission_rejection_message(self, setter_email, flag_name):
+        # This could be queried from the tool.
+        queue_name = "commit-queue"
+        committers_list = self._committers_py_path()
+        message = "%s does not have %s permissions according to %s." % (
+                        setter_email,
+                        flag_name,
+                        urls.view_source_url(committers_list))
+        message += "\n\n- If you do not have %s rights please read %s for instructions on how to use bugzilla flags." % (
+                        flag_name, urls.contribution_guidelines)
+        message += "\n\n- If you have %s rights please correct the error in %s by adding yourself to the file (no review needed).  " % (
+                        flag_name, committers_list)
+        message += "The %s restarts itself every 2 hours.  After restart the %s will correctly respect your %s rights." % (
+                        queue_name, queue_name, flag_name)
+        return message
+
+    def _validate_setter_email(self, patch, result_key, rejection_function):
+        committer = getattr(patch, result_key)()
+        # If the flag is set, and we don't recognize the setter, reject the flag!
+        setter_email = patch._attachment_dictionary.get("%s_email" % result_key)
+        if setter_email and not committer:
+            rejection_function(patch.id(), self._flag_permission_rejection_message(setter_email, result_key))
+            return False
+        return True
+
+    def _reject_patch_if_flags_are_invalid(self, patch):
+        return (self._validate_setter_email(patch, "reviewer", self.reject_patch_from_review_queue)
+            and self._validate_setter_email(patch, "committer", self.reject_patch_from_commit_queue))
+
+    def patches_after_rejecting_invalid_commiters_and_reviewers(self, patches):
+        return [patch for patch in patches if self._reject_patch_if_flags_are_invalid(patch)]
+
+    def reject_patch_from_commit_queue(self,
+                                       attachment_id,
+                                       additional_comment_text=None):
+        comment_text = "Rejecting attachment %s from commit-queue." % attachment_id
+        self.host.bugs.set_flag_on_attachment(attachment_id,
+                                              "commit-queue",
+                                              "-",
+                                              comment_text,
+                                              additional_comment_text)
+
+    def reject_patch_from_review_queue(self,
+                                       attachment_id,
+                                       additional_comment_text=None):
+        comment_text = "Rejecting attachment %s from review queue." % attachment_id
+        self.host.bugs.set_flag_on_attachment(attachment_id,
+                                              'review',
+                                              '-',
+                                              comment_text,
+                                              additional_comment_text)
diff --git a/Tools/Scripts/webkitpy/common/config/committervalidator_unittest.py b/Tools/Scripts/webkitpy/common/config/committervalidator_unittest.py
new file mode 100644
index 0000000..232f077
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/config/committervalidator_unittest.py
@@ -0,0 +1,44 @@
+# 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.host_mock import MockHost
+from .committervalidator import CommitterValidator
+
+
+class CommitterValidatorTest(unittest.TestCase):
+    def test_flag_permission_rejection_message(self):
+        validator = CommitterValidator(MockHost())
+        self.assertEqual(validator._committers_py_path(), "Tools/Scripts/webkitpy/common/config/committers.py")
+        expected_messsage = """foo@foo.com does not have review permissions according to http://trac.webkit.org/browser/trunk/Tools/Scripts/webkitpy/common/config/committers.py.
+
+- If you do not have review rights please read http://webkit.org/coding/contributing.html for instructions on how to use bugzilla flags.
+
+- If you have review rights please correct the error in Tools/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 review rights."""
+        self.assertEqual(validator._flag_permission_rejection_message("foo@foo.com", "review"), expected_messsage)
diff --git a/Tools/Scripts/webkitpy/common/config/contributionareas.py b/Tools/Scripts/webkitpy/common/config/contributionareas.py
new file mode 100644
index 0000000..61a7488
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/config/contributionareas.py
@@ -0,0 +1,217 @@
+# Copyright (c) 2011 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 re
+
+
+class _Intersection(object):
+    def __init__(self, *tokens):
+        self._tokens = tokens
+
+    def matches(self, tokens):
+        for token in self._tokens:
+            if token not in tokens and (token + 's') not in tokens:
+                return False
+        return True
+
+
+class _Area(object):
+    def __init__(self, name, tokens=None):
+        self._name = name
+        self._tokens = tokens if tokens else [self._name_to_token(name)]
+
+    def _name_to_token(self, word):
+        token = word.lower()
+        return token[:-1] if word[-1] == 's' else token
+
+    def matches(self, tokens):
+        # FIXME: Support pluraization properly
+        for token in self._tokens:
+            if isinstance(token, _Intersection):
+                if token.matches(tokens):
+                    return True
+            elif token in tokens or (token + 's') in tokens:
+                return True
+        return False
+
+    def name(self):
+        return self._name
+
+    def tokens(self):
+        return self._tokens
+
+contribution_areas = [
+    _Area('ARM JIT', ['arm']),
+# FIXME: 'Accelerated compositing / GPU acceleration'
+    _Area('Accessibility'),
+    _Area('Android port', ['android']),
+    _Area('Animation', ['animation', 'animator']),
+    _Area('Apple\'s Windows port', ['win', 'windows']),  # FIXME: need to exclude chromium...
+    _Area('Autotools Build', ['autotools']),
+    _Area('Basic types and data structures (WTF)', ['wtf']),
+# FIXME: 'Bidirectional text'
+# FIXME: 'Build/test infrastructure (stuff under Tools/Scripts)'
+    _Area('CMake Build', ['cmakelist']),
+    _Area('CSS (Cascading Style Sheets)', ['css']),
+    _Area('CSS Transforms', [_Intersection('css', 'transforms')]),
+    _Area('CSS/SVG Filters', [_Intersection('css', 'filters'), _Intersection('svg', 'filters')]),
+    _Area('CURL HTTP Backend', ['CURL']),
+    _Area('Resource Cache', [_Intersection('loader', 'cache')]),
+    _Area('Memory Cache', [_Intersection('graphics', 'cache')]),
+    _Area('Cairo'),
+    _Area('Canvas'),
+    _Area('Chromium Linux', [_Intersection('chromium', 'linux')]),
+# FIXME: 'Commit Queue'
+    _Area('Core DOM', ['dom']),
+    _Area('Core Graphics', ['cg']),
+    _Area('Bindings'),
+    _Area('DOM Storage', ['storage']),
+    _Area('Drag and Drop', ['drag']),
+    _Area('DumpRenderTree'),
+    _Area('EFL', ['efl']),
+    _Area('Editing / Selection', ['editing']),
+    _Area('Event Handling', ['event']),
+    _Area('FastMalloc'),
+    _Area('File API', ['fileapi']),
+    _Area('Fonts'),
+    _Area('Forms'),
+# FIXME: 'Frame Flattening'
+    _Area('Frame Loader'),
+# FIXME: 'General' Maybe auto-detect people contributing to all subdirectories?
+    _Area('Geolocation API', ['geolocation']),
+    _Area('Graphics', ['graphics']),
+    _Area('HTML', ['html']),
+    _Area('HTML Parser', [_Intersection('html', 'parser')]),  # FIXME: matches html/track/WebVTTParser.cpp
+    _Area('HTML5 Media Support', ['media']),
+    _Area('History', ['history']),
+# FIXME: 'Hit testing'
+    _Area('Image Decoder', ['imagedecoder']),
+# FIXME: 'Input methods'
+    _Area('JSC Bindings', [_Intersection('bindings', 'js')]),
+    _Area('JavaScriptCore'),
+    _Area('JavaScriptCore Regular Expressions', [_Intersection('JavaScriptCore', 'regexp')]),
+# FIXME: 'Layout tests' but what does it mean to say you're an expert on layout tests? Maybe worked on tools?
+    _Area('Loader', ['loader']),
+    _Area('MathML'),
+    _Area('Memory Use / Leaks', ['leaks']),  # Probably need more tokens
+    _Area('MessagePorts'),
+    _Area('Network', [_Intersection('platform', 'network')]),
+    _Area('new-run-webkit-tests', ['layout_tests']),
+    _Area('OpenVG graphics backend', ['openvg']),
+# FIXME: 'Performance'
+    _Area('Plug-ins', ['plugins']),
+    _Area('Printing', ['printing', 'print']),
+    _Area('Rendering'),
+    _Area('SVG (Scalable Vector Graphics)', ['svg']),
+    _Area('Scrollbars', ['scroll']),
+    _Area('Security'),  # Probably need more tokens
+# FIXME: 'Shadow DOM'
+    _Area('Skia'),
+    _Area('Soup Network Backend', ['soup']),
+# FIXME: 'Spell Checking' just need tokens
+    _Area('Tables', ['htmltable', 'rendertable']),
+# FIXME: 'Text Encoding'
+# FIXME: 'Text Layout'
+    _Area('The Chromium Port', ['chromium']),
+    _Area('The EFLWebKit Port', ['efl']),
+    _Area('The WebKitGTK+ Port', ['gtk']),
+    _Area('The Haiku Port', ['haiku']),
+    _Area('The QtWebKit Port', ['qt']),
+    _Area('The WinCE Port', ['wince']),
+    _Area('The WinCairo Port', ['cairo']),
+    _Area('The wxWebKit Port', ['wx']),
+    _Area('Threading', ['thread']),
+    _Area('Tools'),
+    _Area('Touch Support', ['touch']),
+    _Area('Transforms', ['transforms']),  # There's also CSS transforms
+    _Area('Transitions', ['transitions']),  # This only matches transition events at the moment
+    _Area('URL Parsing', ['KURL']),  # Probably need more tokens
+    _Area('V8', ['v8']),
+    _Area('V8 Bindings', [_Intersection('bindings', 'v8')]),
+    _Area('Web Inspector / Developer Tools', ['inspector']),
+    _Area('Web Timing', ['PerformanceNavigation', 'PerformanceTiming']),  # more tokens?
+    _Area('WebArchive'),
+    _Area('WebCore Icon Database', ['icon']),
+    _Area('WebGL', ['webgl']),
+    _Area('WebKit Websites', ['websites']),
+    _Area('WebKit2'),
+    _Area('WebSQL Databases', [_Intersection('storage', 'database')]),
+    _Area('WebSockets'),
+    _Area('Workers'),
+    _Area('XML'),
+    _Area('XMLHttpRequest'),
+    _Area('XSLT'),
+    _Area('XSSAuditor'),
+    _Area('WebKit API Tests', ['TestWebKitAPI']),
+    _Area('webkit-patch', [_Intersection('webkitpy', 'commands')]),
+]
+
+
+class ContributionAreas(object):
+    def __init__(self, filesystem, table=contribution_areas):
+        self._filesystem = filesystem
+        self._contribution_areas = table
+
+    def names(self):
+        return [area.name() for area in self._contribution_areas]
+
+    def names(self):
+        return [area.name() for area in self._contribution_areas]
+
+    def _split_path(self, path):
+        result = []
+        while path and len(path):
+            next_path, tail = self._filesystem.split(path)
+            if path == next_path:
+                break
+            if tail and len(tail):
+                result.append(tail)
+            path = next_path
+        return result
+
+    def _split_camelcase(self, name, transform=lambda x: x):
+        result = []
+        while name and len(name):
+            m = re.match('^([A-Z][a-z0-9]+)|([A-Z0-9]+(?=([A-Z][a-z0-9]|\.|$)))', name)
+            if m:
+                result.append(transform(m.group(0)))
+                name = name[m.end():]
+            else:
+                return result
+        return result
+
+    def areas_for_touched_files(self, touched_files):
+        areas = set()
+        for file_path in touched_files:
+            split_file_path = self._split_path(file_path)
+            tokenized_file_path = None
+            tokenized_file_path = sum([self._split_camelcase(token, lambda x: x.lower()) for token in split_file_path], [])
+            for area in self._contribution_areas:
+                if area.matches(split_file_path) or area.matches(tokenized_file_path):
+                    areas.add(area.name())
+        return areas
diff --git a/Tools/Scripts/webkitpy/common/config/contributionareas_unittest.py b/Tools/Scripts/webkitpy/common/config/contributionareas_unittest.py
new file mode 100644
index 0000000..c3960d9
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/config/contributionareas_unittest.py
@@ -0,0 +1,61 @@
+# Copyright (c) 2011 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 .contributionareas import _Intersection
+from .contributionareas import _Area
+from .contributionareas import ContributionAreas
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+
+
+class ContributionAreasTest(unittest.TestCase):
+
+    def test_contribution(self):
+        self.assertEqual(_Area('CSS').tokens(), ['css'])
+        self.assertEqual(_Area('Forms', ['input']).tokens(), ['input'])
+
+    def _assert_areas_for_touched_files(self, areas, files, expected_areas):
+        self.assertEqual(areas.areas_for_touched_files(files), set(expected_areas))
+
+    def test_areas_for_touched_files(self):
+        areas = ContributionAreas(MockFileSystem(), [
+            _Area('CSS'),
+            _Area('HTML'),
+            _Area('Forms', ['forms', 'input']),
+            _Area('CSS Transforms', [_Intersection('css', 'transforms')]),
+        ])
+        self._assert_areas_for_touched_files(areas, [], [])
+        self._assert_areas_for_touched_files(areas, ['WebCore/css'], ['CSS'])
+        self._assert_areas_for_touched_files(areas, ['WebCore/html/'], ['HTML'])
+        self._assert_areas_for_touched_files(areas, ['WebCore/css/CSSStyleSelector.cpp', 'WebCore/html/HTMLIFrameElement.h'], ['CSS', 'HTML'])
+        self._assert_areas_for_touched_files(areas, ['WebCore'], [])
+        self._assert_areas_for_touched_files(areas, ['WebCore/html2'], [])
+        self._assert_areas_for_touched_files(areas, ['WebCore/html/HTMLInputElement.cpp'], ['HTML', 'Forms'])
+        self._assert_areas_for_touched_files(areas, ['WebCore/svg/transforms'], [])
+        self._assert_areas_for_touched_files(areas, ['WebCore/css/transforms'], ['CSS', 'CSS Transforms'])
diff --git a/Tools/Scripts/webkitpy/common/config/irc.py b/Tools/Scripts/webkitpy/common/config/irc.py
new file mode 100644
index 0000000..950c573
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/config/irc.py
@@ -0,0 +1,31 @@
+# Copyright (c) 2009 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.
+
+server="irc.freenode.net"
+port=6667
+channel="#webkit"
diff --git a/Tools/Scripts/webkitpy/common/config/orderfile b/Tools/Scripts/webkitpy/common/config/orderfile
new file mode 100644
index 0000000..9fb4977
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/config/orderfile
@@ -0,0 +1,8 @@
+Source*ChangeLog
+Source*
+Tools*ChangeLog
+Tools*
+Websites*ChangeLog
+Websites*
+LayoutTests*ChangeLog
+LayoutTests*
diff --git a/Tools/Scripts/webkitpy/common/config/ports.py b/Tools/Scripts/webkitpy/common/config/ports.py
new file mode 100644
index 0000000..884380e
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/config/ports.py
@@ -0,0 +1,216 @@
+# Copyright (C) 2009, 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.
+#
+# WebKit's Python module for understanding the various ports
+
+import os
+import platform
+import sys
+
+from webkitpy.common.system.executive import Executive
+
+
+class DeprecatedPort(object):
+    results_directory = "/tmp/layout-test-results"
+
+    # FIXME: This is only used by BotInfo.
+    def name(self):
+        return self.__class__
+
+    def flag(self):
+        if self.port_flag_name:
+            return "--port=%s" % self.port_flag_name
+        return None
+
+    # We might need to pass scm into this function for scm.checkout_root
+    def script_path(self, script_name):
+        return os.path.join("Tools", "Scripts", script_name)
+
+    def script_shell_command(self, script_name):
+        script_path = self.script_path(script_name)
+        return Executive.shell_command_for_script(script_path)
+
+    @staticmethod
+    def port(port_name):
+        ports = {
+            "chromium": ChromiumPort,
+            "chromium-android": ChromiumAndroidPort,
+            "chromium-xvfb": ChromiumXVFBPort,
+            "gtk": GtkPort,
+            "mac": MacPort,
+            "win": WinPort,
+            "qt": QtPort,
+            "efl": EflPort,
+        }
+        default_port = {
+            "Windows": WinPort,
+            "Darwin": MacPort,
+        }
+        # Do we really need MacPort as the ultimate default?
+        return ports.get(port_name, default_port.get(platform.system(), MacPort))()
+
+    def makeArgs(self):
+        # FIXME: This shouldn't use a static Executive().
+        args = '--makeargs="-j%s"' % Executive().cpu_count()
+        if os.environ.has_key('MAKEFLAGS'):
+            args = '--makeargs="%s"' % os.environ['MAKEFLAGS']
+        return args
+
+    def update_webkit_command(self, non_interactive=False):
+        return self.script_shell_command("update-webkit")
+
+    def check_webkit_style_command(self):
+        return self.script_shell_command("check-webkit-style")
+
+    def prepare_changelog_command(self):
+        return self.script_shell_command("prepare-ChangeLog")
+
+    def build_webkit_command(self, build_style=None):
+        command = self.script_shell_command("build-webkit")
+        if build_style == "debug":
+            command.append("--debug")
+        if build_style == "release":
+            command.append("--release")
+        return command
+
+    def run_javascriptcore_tests_command(self):
+        return self.script_shell_command("run-javascriptcore-tests")
+
+    def run_webkit_unit_tests_command(self):
+        return None
+
+    def run_webkit_tests_command(self):
+        return self.script_shell_command("run-webkit-tests")
+
+    def run_python_unittests_command(self):
+        return self.script_shell_command("test-webkitpy")
+
+    def run_perl_unittests_command(self):
+        return self.script_shell_command("test-webkitperl")
+
+    def layout_tests_results_path(self):
+        return os.path.join(self.results_directory, "full_results.json")
+
+    def unit_tests_results_path(self):
+        return os.path.join(self.results_directory, "webkit_unit_tests_output.xml")
+
+
+class MacPort(DeprecatedPort):
+    port_flag_name = "mac"
+
+
+class WinPort(DeprecatedPort):
+    port_flag_name = "win"
+
+
+class GtkPort(DeprecatedPort):
+    port_flag_name = "gtk"
+
+    def build_webkit_command(self, build_style=None):
+        command = super(GtkPort, self).build_webkit_command(build_style=build_style)
+        command.append("--gtk")
+        command.append("--update-gtk")
+        command.append(super(GtkPort, self).makeArgs())
+        return command
+
+    def run_webkit_tests_command(self):
+        command = super(GtkPort, self).run_webkit_tests_command()
+        command.append("--gtk")
+        return command
+
+
+class QtPort(DeprecatedPort):
+    port_flag_name = "qt"
+
+    def build_webkit_command(self, build_style=None):
+        command = super(QtPort, self).build_webkit_command(build_style=build_style)
+        command.append("--qt")
+        command.append(super(QtPort, self).makeArgs())
+        return command
+
+
+class EflPort(DeprecatedPort):
+    port_flag_name = "efl"
+
+    def build_webkit_command(self, build_style=None):
+        command = super(EflPort, self).build_webkit_command(build_style=build_style)
+        command.append("--efl")
+        command.append("--update-efl")
+        command.append(super(EflPort, self).makeArgs())
+        return command
+
+
+class ChromiumPort(DeprecatedPort):
+    port_flag_name = "chromium"
+
+    def update_webkit_command(self, non_interactive=False):
+        command = super(ChromiumPort, self).update_webkit_command(non_interactive=non_interactive)
+        command.append("--chromium")
+        if non_interactive:
+            command.append("--force-update")
+        return command
+
+    def build_webkit_command(self, build_style=None):
+        command = super(ChromiumPort, self).build_webkit_command(build_style=build_style)
+        command.append("--chromium")
+        command.append("--update-chromium")
+        return command
+
+    def run_webkit_tests_command(self):
+        # Note: This could be run-webkit-tests now.
+        command = self.script_shell_command("new-run-webkit-tests")
+        command.append("--chromium")
+        command.append("--skip-failing-tests")
+        return command
+
+    def run_webkit_unit_tests_command(self):
+        return self.script_shell_command("run-chromium-webkit-unit-tests")
+
+    def run_javascriptcore_tests_command(self):
+        return None
+
+
+class ChromiumAndroidPort(ChromiumPort):
+    port_flag_name = "chromium-android"
+
+    def update_webkit_command(self, non_interactive=False):
+        command = super(ChromiumAndroidPort, self).update_webkit_command(non_interactive=non_interactive)
+        command.append("--chromium-android")
+        return command
+
+    def build_webkit_command(self, build_style=None):
+        command = super(ChromiumAndroidPort, self).build_webkit_command(build_style=build_style)
+        command.append("--chromium-android")
+        return command
+
+
+class ChromiumXVFBPort(ChromiumPort):
+    port_flag_name = "chromium-xvfb"
+
+    def run_webkit_tests_command(self):
+        return ["xvfb-run"] + super(ChromiumXVFBPort, self).run_webkit_tests_command()
diff --git a/Tools/Scripts/webkitpy/common/config/ports_mock.py b/Tools/Scripts/webkitpy/common/config/ports_mock.py
new file mode 100644
index 0000000..1d14311
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/config/ports_mock.py
@@ -0,0 +1,67 @@
+# Copyright (C) 2011 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 MockPort(object):
+    results_directory = "/mock-results"
+
+    def name(self):
+        return "MockPort"
+
+    def layout_tests_results_path(self):
+        return "/mock-results/full_results.json"
+
+    def unit_tests_results_path(self):
+        return "/mock-results/webkit_unit_tests_output.xml"
+
+    def check_webkit_style_command(self):
+        return ["mock-check-webkit-style"]
+
+    def update_webkit_command(self, non_interactive=False):
+        return ["mock-update-webkit"]
+
+    def build_webkit_command(self, build_style=None):
+        return ["mock-build-webkit"]
+
+    def prepare_changelog_command(self):
+        return ['mock-prepare-ChangeLog']
+
+    def run_python_unittests_command(self):
+        return ['mock-test-webkitpy']
+
+    def run_perl_unittests_command(self):
+        return ['mock-test-webkitperl']
+
+    def run_javascriptcore_tests_command(self):
+        return ['mock-run-javacriptcore-tests']
+
+    def run_webkit_unit_tests_command(self):
+        return ['mock-run-webkit-unit-tests']
+
+    def run_webkit_tests_command(self):
+        return ['mock-run-webkit-tests']
diff --git a/Tools/Scripts/webkitpy/common/config/ports_unittest.py b/Tools/Scripts/webkitpy/common/config/ports_unittest.py
new file mode 100644
index 0000000..2720523
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/config/ports_unittest.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, 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.config.ports import *
+
+
+class DeprecatedPortTest(unittest.TestCase):
+    def test_mac_port(self):
+        self.assertEquals(MacPort().flag(), "--port=mac")
+        self.assertEquals(MacPort().run_webkit_tests_command(), DeprecatedPort().script_shell_command("run-webkit-tests"))
+        self.assertEquals(MacPort().build_webkit_command(), DeprecatedPort().script_shell_command("build-webkit"))
+        self.assertEquals(MacPort().build_webkit_command(build_style="debug"), DeprecatedPort().script_shell_command("build-webkit") + ["--debug"])
+        self.assertEquals(MacPort().build_webkit_command(build_style="release"), DeprecatedPort().script_shell_command("build-webkit") + ["--release"])
+
+    def test_gtk_port(self):
+        self.assertEquals(GtkPort().flag(), "--port=gtk")
+        self.assertEquals(GtkPort().run_webkit_tests_command(), DeprecatedPort().script_shell_command("run-webkit-tests") + ["--gtk"])
+        self.assertEquals(GtkPort().build_webkit_command(), DeprecatedPort().script_shell_command("build-webkit") + ["--gtk", "--update-gtk", DeprecatedPort().makeArgs()])
+        self.assertEquals(GtkPort().build_webkit_command(build_style="debug"), DeprecatedPort().script_shell_command("build-webkit") + ["--debug", "--gtk", "--update-gtk", DeprecatedPort().makeArgs()])
+
+    def test_efl_port(self):
+        self.assertEquals(EflPort().flag(), "--port=efl")
+        self.assertEquals(EflPort().build_webkit_command(), DeprecatedPort().script_shell_command("build-webkit") + ["--efl", "--update-efl", DeprecatedPort().makeArgs()])
+        self.assertEquals(EflPort().build_webkit_command(build_style="debug"), DeprecatedPort().script_shell_command("build-webkit") + ["--debug", "--efl", "--update-efl", DeprecatedPort().makeArgs()])
+
+    def test_qt_port(self):
+        self.assertEquals(QtPort().flag(), "--port=qt")
+        self.assertEquals(QtPort().run_webkit_tests_command(), DeprecatedPort().script_shell_command("run-webkit-tests"))
+        self.assertEquals(QtPort().build_webkit_command(), DeprecatedPort().script_shell_command("build-webkit") + ["--qt", DeprecatedPort().makeArgs()])
+        self.assertEquals(QtPort().build_webkit_command(build_style="debug"), DeprecatedPort().script_shell_command("build-webkit") + ["--debug", "--qt", DeprecatedPort().makeArgs()])
+
+    def test_chromium_port(self):
+        self.assertEquals(ChromiumPort().flag(), "--port=chromium")
+        self.assertEquals(ChromiumPort().run_webkit_tests_command(), DeprecatedPort().script_shell_command("new-run-webkit-tests") + ["--chromium", "--skip-failing-tests"])
+        self.assertEquals(ChromiumPort().build_webkit_command(), DeprecatedPort().script_shell_command("build-webkit") + ["--chromium", "--update-chromium"])
+        self.assertEquals(ChromiumPort().build_webkit_command(build_style="debug"), DeprecatedPort().script_shell_command("build-webkit") + ["--debug", "--chromium", "--update-chromium"])
+        self.assertEquals(ChromiumPort().update_webkit_command(), DeprecatedPort().script_shell_command("update-webkit") + ["--chromium"])
+
+    def test_chromium_android_port(self):
+        self.assertEquals(ChromiumAndroidPort().build_webkit_command(), ChromiumPort().build_webkit_command() + ["--chromium-android"])
+        self.assertEquals(ChromiumAndroidPort().update_webkit_command(), ChromiumPort().update_webkit_command() + ["--chromium-android"])
+
+    def test_chromium_xvfb_port(self):
+        self.assertEquals(ChromiumXVFBPort().run_webkit_tests_command(), ['xvfb-run'] + DeprecatedPort().script_shell_command('new-run-webkit-tests') + ['--chromium', '--skip-failing-tests'])
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/common/config/urls.py b/Tools/Scripts/webkitpy/common/config/urls.py
new file mode 100644
index 0000000..88ad373
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/config/urls.py
@@ -0,0 +1,82 @@
+# 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 re
+
+
+def view_source_url(local_path):
+    return "http://trac.webkit.org/browser/trunk/%s" % local_path
+
+
+def view_revision_url(revision_number):
+    return "http://trac.webkit.org/changeset/%s" % revision_number
+
+
+def chromium_results_zip_url(builder_name):
+    return 'http://build.chromium.org/f/chromium/layout_test_results/%s/layout-test-results.zip' % builder_name
+
+chromium_lkgr_url = "http://chromium-status.appspot.com/lkgr"
+contribution_guidelines = "http://webkit.org/coding/contributing.html"
+
+bug_server_domain = "webkit.org"
+bug_server_host = "bugs." + bug_server_domain
+_bug_server_regex = "https?://%s/" % re.sub('\.', '\\.', bug_server_host)
+bug_server_url = "https://%s/" % bug_server_host
+bug_url_long = _bug_server_regex + r"show_bug\.cgi\?id=(?P<bug_id>\d+)(&ctype=xml|&excludefield=attachmentdata)*"
+bug_url_short = r"https?\://%s/b/(?P<bug_id>\d+)" % bug_server_domain
+
+attachment_url = _bug_server_regex + r"attachment\.cgi\?id=(?P<attachment_id>\d+)(&action=(?P<action>\w+))?"
+direct_attachment_url = r"https?://bug-(?P<bug_id>\d+)-attachments.%s/attachment\.cgi\?id=(?P<attachment_id>\d+)" % bug_server_domain
+
+buildbot_url = "http://build.webkit.org"
+chromium_buildbot_url = "http://build.chromium.org/p/chromium.webkit"
+
+omahaproxy_url = "http://omahaproxy.appspot.com/"
+
+def parse_bug_id(string):
+    if not string:
+        return None
+    match = re.search(bug_url_short, string)
+    if match:
+        return int(match.group('bug_id'))
+    match = re.search(bug_url_long, string)
+    if match:
+        return int(match.group('bug_id'))
+    return None
+
+
+def parse_attachment_id(string):
+    if not string:
+        return None
+    match = re.search(attachment_url, string)
+    if match:
+        return int(match.group('attachment_id'))
+    match = re.search(direct_attachment_url, string)
+    if match:
+        return int(match.group('attachment_id'))
+    return None
diff --git a/Tools/Scripts/webkitpy/common/config/urls_unittest.py b/Tools/Scripts/webkitpy/common/config/urls_unittest.py
new file mode 100644
index 0000000..2b94b86
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/config/urls_unittest.py
@@ -0,0 +1,61 @@
+# Copyright (C) 2012 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 .urls import parse_bug_id, parse_attachment_id
+
+
+class URLsTest(unittest.TestCase):
+    def test_parse_bug_id(self):
+        # FIXME: These would be all better as doctests
+        self.assertEquals(12345, parse_bug_id("http://webkit.org/b/12345"))
+        self.assertEquals(12345, parse_bug_id("foo\n\nhttp://webkit.org/b/12345\nbar\n\n"))
+        self.assertEquals(12345, parse_bug_id("http://bugs.webkit.org/show_bug.cgi?id=12345"))
+        self.assertEquals(12345, parse_bug_id("http://bugs.webkit.org/show_bug.cgi?id=12345&ctype=xml"))
+        self.assertEquals(12345, parse_bug_id("http://bugs.webkit.org/show_bug.cgi?id=12345&ctype=xml&excludefield=attachmentdata"))
+        self.assertEquals(12345, parse_bug_id("http://bugs.webkit.org/show_bug.cgi?id=12345excludefield=attachmentdata&ctype=xml"))
+
+        # Our url parser is super-fragile, but at least we're testing it.
+        self.assertEquals(None, parse_bug_id("http://www.webkit.org/b/12345"))
+        self.assertEquals(None, parse_bug_id("http://bugs.webkit.org/show_bug.cgi?ctype=xml&id=12345"))
+        self.assertEquals(None, parse_bug_id("http://bugs.webkit.org/show_bug.cgi?ctype=xml&id=12345&excludefield=attachmentdata"))
+        self.assertEquals(None, parse_bug_id("http://bugs.webkit.org/show_bug.cgi?ctype=xml&excludefield=attachmentdata&id=12345"))
+        self.assertEquals(None, parse_bug_id("http://bugs.webkit.org/show_bug.cgi?excludefield=attachmentdata&ctype=xml&id=12345"))
+        self.assertEquals(None, parse_bug_id("http://bugs.webkit.org/show_bug.cgi?excludefield=attachmentdata&id=12345&ctype=xml"))
+
+    def test_parse_attachment_id(self):
+        self.assertEquals(12345, parse_attachment_id("https://bugs.webkit.org/attachment.cgi?id=12345&action=review"))
+        self.assertEquals(12345, parse_attachment_id("https://bugs.webkit.org/attachment.cgi?id=12345&action=edit"))
+        self.assertEquals(12345, parse_attachment_id("https://bugs.webkit.org/attachment.cgi?id=12345&action=prettypatch"))
+        self.assertEquals(12345, parse_attachment_id("https://bugs.webkit.org/attachment.cgi?id=12345&action=diff"))
+
+        # Direct attachment links are hosted from per-bug subdomains:
+        self.assertEquals(12345, parse_attachment_id("https://bug-23456-attachments.webkit.org/attachment.cgi?id=12345"))
+        # Make sure secure attachment URLs work too.
+        self.assertEquals(12345, parse_attachment_id("https://bug-23456-attachments.webkit.org/attachment.cgi?id=12345&t=Bqnsdkl9fs"))
diff --git a/Tools/Scripts/webkitpy/common/config/watchlist b/Tools/Scripts/webkitpy/common/config/watchlist
new file mode 100755
index 0000000..854a812
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/config/watchlist
@@ -0,0 +1,357 @@
+#  -*- mode: Python;-*-
+#
+# When editing this file, please run the following command to make sure you
+# haven't introduced any syntax errors:
+#
+# ./Tools/Scripts/check-webkit-style
+#
+# If you want to test your regular expressions, you can edit various files and
+# then try following command:
+#
+# ./Tools/Scripts/webkit-patch apply-watchlist-local
+#
+{
+    "DEFINITIONS": {
+        "ChromiumGraphics": {
+            "filename": r"Source/WebCore/platform/graphics/chromium/",
+        },
+        "ChromiumPublicApi": {
+            "filename": r"Source/WebKit/chromium/public/"
+                        r"|Source/Platform/chromium/public/",
+        },
+        "AppleMacPublicApi": {
+            "filename": r"Source/WebCore/bindings/objc/PublicDOMInterfaces.h"
+        },
+        "Forms": {
+            "filename": r"Source/WebCore/html/HTML(DataList|FieldSet|Input|Keygen|Label|Legend|OptGroup|Option|Output|Select|TextArea)Element\."
+                        r"|Source/WebCore/html/.*Form[A-Z].*\."
+                        r"|Source/WebCore/html/\w*InputType\."
+                        r"|Source/WebCore/html/shadow/(SliderThumbElement|TextControlInnerElements)\."
+                        r"|Source/WebCore/rendering/Render(FileUploadControl|ListBox|MenuList|Slider|TextControl.*)\."
+        },
+        "GStreamerGraphics": {
+            "filename": r"Source/WebCore/platform/graphics/gstreamer/",
+        },
+        "WebIDL": {
+            "filename": r"Source/WebCore/(?!inspector)(?!testing).*\.idl"
+        },
+        "ThreadingFiles": {
+            "filename": r"Source/JavaScriptCore/wtf/ThreadSpecific\."
+                        r"|Source/JavaScriptCore/wtf/ThreadSafeRefCounted\."
+                        r"|Source/JavaScriptCore/wtf/ThreadingPrimitives\."
+                        r"|Source/JavaScriptCore/wtf/Threading\."
+                        r"|Source/WebCore/dom/CrossThreadTask\."
+                        r"|Source/WebCore/platform/CrossThreadCopier\.",
+        },
+        "ThreadingUsage": {
+            # The intention of this regex is to detect places where people are using common threading mechanisms,
+            # so that one can look them over for common mistakes. This list is long and likely to get longer over time.
+            # Note the negative look-ahead to avoid new mentions of the files (for builds or includes).
+            "more": r"(AllowCrossThreadAccess|AtomicallyInitialize|CrossThreadCopier|CrossThreadRefCounted|Mutex|ReadWriteLock|ThreadCondition|ThreadSafeRefCounted|ThreadSpecific"
+                    r"|createCallbackTask|crossThreadString|deprecatedTurnOffVerifier|threadsafeCopy)(?!\.(h|cpp))",
+        },
+        "WatchListScript": {
+            "filename": r"Tools/Scripts/webkitpy/common/watchlist/",
+        },
+        "webkitpy": {
+            "filename": r"Tools/Scripts/webkitpy/",
+        },
+        "webkitperl": {
+            "filename": r"Tools/Scripts/webkitperl/"
+                        r"|Tools/Scripts/webkitdirs.pm"
+                        r"|Tools/Scripts/VCSUtils.pm"
+                        r"|Tools/Scripts/test-webkitperl",
+        },
+        "SVNScripts": {
+            "filename": r"Tools/Scripts/svn-.*",
+        },
+        "TestFailures": {
+            "filename": r"Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/",
+        },
+        "SecurityCritical": {
+            "more": r"[Ss]ecurityOrigin(?!\.(h|cpp))",
+            "less": r"[Ss]ecurityOrigin(?!\.(h|cpp))",
+            "filename": r"XSS|[Ss]ecurity",
+        },
+        "XSS": {
+            "filename": r".*XSS",
+        },
+        "SkiaGraphics": {
+            "filename": r"Source/WebCore/platform/graphics/skia/"
+                        r"|Source/WebCore/platform/graphics/filters/skia/",
+        },
+        "V8Bindings": {
+            "filename": r"Source/WebCore/bindings/v8/",
+        },
+        "BindingsScripts": {
+            "filename": r"Source/WebCore/bindings/scripts/",
+        },
+        "FrameLoader": {
+            "more": r"FrameLoader\.(cpp|h)",
+        },
+        "Loader": {
+            "filename": r"Source/WebCore/loader/",
+        },
+        "Rendering": {
+            "filename": r"Source/WebCore/rendering/",
+        },
+        "StyleChecker": {
+            "filename": r"Tools/Scripts/webkitpy/style/",
+        },
+        "GtkWebKit2PublicAPI": {
+            "filename": r"Source/WebKit2/UIProcess/API/gtk/",
+        },
+        "QtBuildSystem": {
+            # Project files for each target are intentionally left out, as those
+            # mostly list source and header files, which would just add noise.
+            "filename": r"Tools/qmake/"
+                        r"|WebKit.pro",
+        },
+        "QtWebKit2PublicAPI": {
+            "filename": r"Source/WebKit2/UIProcess/API/qt/"
+                        r"|Source/WebKit2/UIProcess/API/cpp/qt/"
+                        r"|Source/WebKit2/UIProcess/API/C/qt/",
+        },
+        "QtGraphics": {
+            "filename": r"Source/WebCore/platform/graphics/qt/"
+                        r"|Source/WebKit2/WebProcess/WebPage/CoordinatedGraphics/"
+                        r"|Source/WebKit2/UIProcess/CoordinatedGraphics",
+        },
+        "TextureMapper": {
+            "filename": r"Source/WebCore/platform/graphics/texmap/",
+        },
+        "OpenGL": {
+            "filename": r"Source/WebCore/platform/graphics/opengl/",
+        },
+        "QtWebKit2PlatformSpecific": {
+            "filename": r"Source/WebKit2/.*\.(pri|pro)"
+                        r"|Source/WebKit2/Platform/qt/"
+                        r"|Source/WebKit2/qt/"
+                        r"|Source/WebKit2/PluginProcess/qt/"
+                        r"|Source/WebKit2/Platform/qt/"
+                        r"|Source/WebKit2/Shared/API/c/qt/"
+                        r"|Source/WebKit2/Shared/qt/"
+                        r"|Source/WebKit2/WebProcess/InjectedBundle/qt/"
+                        r"|Source/WebKit2/WebProcess/FullScreen/qt/"
+                        r"|Source/WebKit2/WebProcess/WebPage/qt/"
+                        r"|Source/WebKit2/WebProcess/qt/"
+                        r"|Source/WebKit2/WebProcess/Plugins/Netscape/qt/"
+                        r"|Source/WebKit2/WebProcess/Downloads/qt/"
+                        r"|Source/WebKit2/WebProcess/WebCoreSupport/qt/"
+                        r"|Source/WebKit2/WebProcess/Cookies/qt/"
+                        r"|Source/WebKit2/UIProcess/qt/"
+                        r"|Source/WebKit2/UIProcess/Plugins/qt/"
+                        r"|Source/WebKit2/UIProcess/Launcher/qt/",
+        },
+        "CSS": {
+            "filename": r"Source/WebCore/css/",
+        },
+        "DOMAttributes": {
+            "filename": r"Source/WebCore/dom/.*Attr.*"
+                        r"|Source/WebCore/dom/NamedNodeMap\.(cpp|h|idl)"
+                        r"|Source/WebCore/dom/Element\.(cpp|h|idl)",
+        },
+        "EFL": {
+            "filename": r"Source/WebKit/efl/"
+                        r"|Source/WebCore/platform/efl/"
+                        r"|Source/WTF/wtf/efl/"
+                        r"|Tools/EWebLauncher"
+                        r"|Tools/DumpRenderTree/efl/"
+                        r"|LayoutTests/platform/efl/",
+        },
+        "EFLWebKit2PublicAPI": {
+            "filename": r"Source/WebKit2/UIProcess/API/efl/"
+                        r"|Source/WebKit2/UIProcess/API/C/efl/",
+        },
+        "EFLWebKit2PlatformSpecific": {
+            "filename": r"Source/WebKit2/.*\.(cmake|txt)"
+                        r"|Source/WebKit2/Platform/efl/"
+                        r"|Source/WebKit2/efl/"
+                        r"|Source/WebKit2/Shared/API/c/efl/"
+                        r"|Source/WebKit2/Shared/efl/"
+                        r"|Source/WebKit2/WebProcess/InjectedBundle/efl/"
+                        r"|Source/WebKit2/WebProcess/WebPage/efl/"
+                        r"|Source/WebKit2/WebProcess/efl/"
+                        r"|Source/WebKit2/WebProcess/Downloads/efl/"
+                        r"|Source/WebKit2/WebProcess/WebCoreSupport/efl/"
+                        r"|Source/WebKit2/UIProcess/efl/"
+                        r"|Source/WebKit2/UIProcess/Launcher/efl/",
+        },
+        "CMake": {
+            "filename": r".*CMakeLists\w*\.txt"
+                        r"|.*\w+\.cmake"
+                        r"|Source/cmake/",
+        },
+        "Selectors": {
+            "filename": r"Source/WebCore/css/CSSSelector*"
+                        r"|Source/WebCore/css/SelectorChecker.*"
+                        r"|Source/WebCore/css/StyleResolver.*"
+                        r"|Source/WebCore/dom/SelectorQuery.*",
+        },
+        "SoupNetwork": {
+            "filename": r"Source/WebCore/platform/network/soup/",
+        },
+        "ScrollingCoordinator": {
+            "filename": r"Source/WebCore/page/scrolling/",
+        },
+        "WebKitGTKTranslations": {
+            "filename": r"Source/WebKit/gtk/po/",
+        },
+        "Media": {
+            "filename": r"(Source|LayoutTests)/.*([Mm]edia|[Aa]udio|[Vv]ideo)",
+        },
+        "MathML": {
+            "filename": r"(Source|LayoutTests|Websites)/.*mathml",
+        },
+        "Editing": {
+            "filename": r"Source/WebCore/editing/",
+        },
+        "BlackBerry": {
+            "filename": r"Source/WebKit/blackberry/"
+                        r"|Source/WebCore/page/blackberry"
+                        r"|Source/WebCore/history/blackberry"
+                        r"|Source/WebCore/plugins/blackberry"
+                        r"|Source/WebCore/editing/blackberry"
+                        r"|Source/WebCore/Resources/blackberry"
+                        r"|Source/WebCore/platform/image-decoders/blackberry"
+                        r"|Source/WebCore/platform/blackberry"
+                        r"|Source/WebCore/platform/text/blackberry"
+                        r"|Source/WebCore/platform/network/blackberry"
+                        r"|Source/WebCore/platform/graphics/blackberry"
+                        r"|Source/WTF/wtf/blackberry"
+                        r"|ManualTests/blackberry"
+                        r"|Tools/DumpRenderTree/blackberry"
+                        r"|LayoutTests/platform/blackberry",
+        },
+        "NetworkInfo": {
+            "filename": r"Source/WebCore/Modules/networkinfo",
+        },
+        "Battery": {
+            "filename": r"Source/WebCore/Modules/battery",
+        },
+        "WTF": {
+            "filename": r"Source/WTF/wtf",
+        },
+        "WebGL": {
+            "filename": r"Source/WebCore/html/canvas/.*WebGL.*"
+                        r"|Source/WebCore/bindings/js/.*WebGL.*"
+                        r"|Source/WebCore/platform/graphics/gpu"
+                        r"|Source/WebCore/platform/graphics/opengl"
+                        r"|Source/WebCore/platform/graphics/ANGLE.*"
+                        r"|Source/WebCore/platform/graphics/.*GraphicsContext3D.*"
+                        r"|Source/ThirdParty/ANGLE",
+        },
+        "Filters": {
+            "filename": r"Source/WebCore/platform/graphics/filters"
+                        r"|Source/WebCore/rendering/.*Filter.*"
+                        r"|Source/WebCore/rendering/style/.*Filter.*"
+                        r"|Source/WebCore/rendering/svg/.*Filter.*"
+                        r"|Source/WebCore/svg/graphics/filters"
+                        r"|Source/WebCore/svg/graphics/.*Filter.*",
+        },
+        "TouchAdjustment": {
+            "filename": r"Source/WebCore/page/TouchAdjustment.*"
+                        r"|LayoutTests/touchadjustment"
+                        r"|Source/WebKit/blackberry/WebKitSupport/FatFingers.*",
+        },
+        "SVG": {
+            "filename": r"Source/WebCore/svg"
+                        r"|Source/WebCore/rendering/svg",
+        },
+        "WebInspectorAPI": {
+            "filename": r"Source/WebCore/inspector/*.json"
+                        r"|Source/WebCore/inspector/*.idl",
+        },
+        "WebSocket": {
+            "filename": r"Source/WebCore/Modules/websockets"
+                        r"|Source/WebCore/platform/network/(|.+/)SocketStream.*",
+        },
+        "MediaStream": {
+            "filename": r"Source/WebCore/Modules/mediastream"
+                        r"|Source/WebCore/platform/mediastream"
+                        r"|LayoutTests/fast/mediastream",
+        },
+        "Accessibility": {
+            "filename": r"Source/WebCore/accessibility"
+                        r"|LayoutTests/.*accessibility",
+        },
+        "Cairo": {
+            "filename": r"Source/WebCore/platform/graphics/cairo",
+        },
+        "Harfbuzz": {
+            "filename": r"Source/WebCore/platform/graphics/harfbuzz",
+        }
+    },
+    "CC_RULES": {
+        # Note: All email addresses listed must be registered with bugzilla.
+        # Specifically, levin@chromium.org and levin+threading@chromium.org are
+        # two different accounts as far as bugzilla is concerned.
+        "Accessibility": [ "cfleizach@apple.com", "dmazzoni@google.com", "apinheiro@igalia.com", "jdiggs@igalia.com" ],
+        "AppleMacPublicApi": [ "timothy@apple.com" ],
+        "Battery": [ "gyuyoung.kim@samsung.com" ],
+        "BlackBerry": [ "mifenton@rim.com", "rwlbuis@gmail.com", "tonikitoo@webkit.org" ],
+        "Cairo": [ "dominik.rottsches@intel.com" ],
+        "CMake": [ "rakuco@webkit.org", "gyuyoung.kim@samsung.com" ],
+        "CSS": [ "alexis@webkit.org", "macpherson@chromium.org", "cmarcelo@webkit.org"],
+        "ChromiumGraphics": [ "jamesr@chromium.org", "cc-bugs@chromium.org" ],
+        "ChromiumPublicApi": [ "abarth@webkit.org", "dglazkov@chromium.org", "fishd@chromium.org", "jamesr@chromium.org", "tkent+wkapi@chromium.org" ],
+        "DOMAttributes": [ "cmarcelo@webkit.org", ],
+        "EFL": [ "rakuco@webkit.org", "gyuyoung.kim@samsung.com" ],
+        "EFLWebKit2PlatformSpecific": [ "gyuyoung.kim@samsung.com", "rakuco@webkit.org" ],
+        "EFLWebKit2PublicAPI": [ "gyuyoung.kim@samsung.com", "rakuco@webkit.org" ],
+        "Editing": [ "mifenton@rim.com" ],
+        "Filters": [ "dino@apple.com" ],
+        "Forms": [ "tkent@chromium.org", "mifenton@rim.com" ],
+        "FrameLoader": [ "abarth@webkit.org", "japhet@chromium.org" ],
+        "GStreamerGraphics": [ "alexis@webkit.org", "pnormand@igalia.com", "gns@gnome.org", "mrobinson@webkit.org" ],
+        "GtkWebKit2PublicAPI": [ "cgarcia@igalia.com", "gns@gnome.org", "mrobinson@webkit.org" ],
+        "Harfbuzz": [ "dominik.rottsches@intel.com" ],
+        "Loader": [ "japhet@chromium.org" ],
+        "MathML": [ "dbarton@mathscribe.com" ],
+        "Media": [ "feature-media-reviews@chromium.org", "eric.carlson@apple.com" ],
+        "MediaStream": [ "tommyw@google.com", "hta@google.com" ],
+        "NetworkInfo": [ "gyuyoung.kim@samsung.com" ],
+        "OpenGL" : [ "noam.rosenthal@nokia.com", "dino@apple.com" ],
+        "QtBuildSystem" : [ "vestbo@webkit.org", "abecsi@webkit.org" ],
+        "QtGraphics" : [ "noam.rosenthal@nokia.com" ],
+        "QtWebKit2PlatformSpecific": [ "alexis@webkit.org", "cmarcelo@webkit.org", "abecsi@webkit.org" ],
+        "QtWebKit2PublicAPI": [ "alexis@webkit.org", "cmarcelo@webkit.org", "abecsi@webkit.org" ],
+        "Rendering": [ "eric@webkit.org" ],
+        "SVG": ["schenney@chromium.org", "pdr@google.com", "fmalita@chromium.org", "dominik.rottsches@intel.com" ],
+        "SVNScripts": [ "dbates@webkit.org" ],
+        "ScrollingCoordinator": [ "andersca@apple.com", "jamesr@chromium.org", "tonikitoo@webkit.org" ],
+        "SecurityCritical": [ "abarth@webkit.org" ],
+        "SkiaGraphics": [ "senorblanco@chromium.org" ],
+        "Selectors": [ "allan.jensen@digia.com" ],
+        "SoupNetwork": [ "rakuco@webkit.org", "gns@gnome.org", "mrobinson@webkit.org", "danw@gnome.org" ],
+        "StyleChecker": [ "levin@chromium.org", ],
+        "TestFailures": [ "abarth@webkit.org", "dglazkov@chromium.org" ],
+        "TextureMapper" : [ "noam.rosenthal@nokia.com" ],
+        "ThreadingFiles|ThreadingUsage": [ "levin+threading@chromium.org", ],
+        "TouchAdjustment" : [ "allan.jensen@digia.com" ],
+        "V8Bindings|BindingsScripts": [ "abarth@webkit.org", "japhet@chromium.org", "haraken@chromium.org" ],
+        "WTF": [ "benjamin@webkit.org",],
+        "WatchListScript": [ "levin+watchlist@chromium.org", ],
+        "WebGL": [ "dino@apple.com" ],
+        "WebIDL": [ "abarth@webkit.org", "ojan@chromium.org" ],
+        "WebInspectorAPI": [ "timothy@apple.com", "joepeck@webkit.org" ],
+        "WebKitGTKTranslations": [ "gns@gnome.org", "mrobinson@webkit.org" ],
+        "WebSocket": [ "yutak@chromium.org" ],
+        "XSS": [ "dbates@webkit.org" ],
+        "webkitperl": [ "dbates@webkit.org" ],
+        "webkitpy": [ "abarth@webkit.org", "ojan@chromium.org", "dpranke@chromium.org" ],
+    },
+    "MESSAGE_RULES": {
+        "ChromiumPublicApi": [ "Please wait for approval from abarth@webkit.org, dglazkov@chromium.org, "
+                               "fishd@chromium.org, jamesr@chromium.org or tkent@chromium.org before "
+                               "submitting, as this patch contains changes to the Chromium public API. "
+                               "See also https://trac.webkit.org/wiki/ChromiumWebKitAPI." ],
+        "AppleMacPublicApi": [ "Please wait for approval from timothy@apple.com (or another member "
+                               "of the Apple Safari Team) before submitting "
+                               "because this patch contains changes to the Apple Mac "
+                               "WebKit.framework public API.", ],
+        "GtkWebKit2PublicAPI": [ "Thanks for the patch. If this patch contains new public API "
+                                 "please make sure it follows the guidelines for new WebKit2 GTK+ API. "
+                                 "See http://trac.webkit.org/wiki/WebKitGTK/AddingNewWebKit2API", ],
+    },
+}
diff --git a/Tools/Scripts/webkitpy/common/editdistance.py b/Tools/Scripts/webkitpy/common/editdistance.py
new file mode 100644
index 0000000..2eccca8
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/editdistance.py
@@ -0,0 +1,47 @@
+# Copyright (c) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from array import array
+
+
+def edit_distance(str1, str2):
+    unsignedShort = 'h'
+    distances = [array(unsignedShort, (0,) * (len(str2) + 1)) for i in range(0, len(str1) + 1)]
+    # distances[0][0] = 0 since distance between str1[:0] and str2[:0] is 0
+    for i in range(1, len(str1) + 1):
+        distances[i][0] = i  # Distance between str1[:i] and str2[:0] is i
+
+    for j in range(1, len(str2) + 1):
+        distances[0][j] = j  # Distance between str1[:0] and str2[:j] is j
+
+    for i in range(0, len(str1)):
+        for j in range(0, len(str2)):
+            diff = 0 if str1[i] == str2[j] else 1
+            # Deletion, Insertion, Identical / Replacement
+            distances[i + 1][j + 1] = min(distances[i + 1][j] + 1, distances[i][j + 1] + 1, distances[i][j] + diff)
+    return distances[len(str1)][len(str2)]
diff --git a/Tools/Scripts/webkitpy/common/editdistance_unittest.py b/Tools/Scripts/webkitpy/common/editdistance_unittest.py
new file mode 100644
index 0000000..4ae6441
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/editdistance_unittest.py
@@ -0,0 +1,43 @@
+# 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.editdistance import edit_distance
+
+
+class EditDistanceTest(unittest.TestCase):
+    def test_edit_distance(self):
+        self.assertEqual(edit_distance('', 'aa'), 2)
+        self.assertEqual(edit_distance('aa', ''), 2)
+        self.assertEqual(edit_distance('a', 'ab'), 1)
+        self.assertEqual(edit_distance('ab', 'a'), 1)
+        self.assertEqual(edit_distance('ab', 'aa'), 1)
+        self.assertEqual(edit_distance('aa', 'ab'), 1)
+        self.assertEqual(edit_distance('abd', 'abcdef'), 3)
+        self.assertEqual(edit_distance('abcdef', 'abd'), 3)
diff --git a/Tools/Scripts/webkitpy/common/find_files.py b/Tools/Scripts/webkitpy/common/find_files.py
new file mode 100644
index 0000000..32ce4d1
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/find_files.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""This module is used to find files used by run-webkit-tests and
+perftestrunner. It exposes one public function - find() - which takes
+an optional list of paths, optional set of skipped directories and optional
+filter callback.
+
+If a list is passed in, the returned list of files is constrained to those
+found under the paths passed in. i.e. calling find(["LayoutTests/fast"])
+will only return files under that directory.
+
+If a set of skipped directories is passed in, the function will filter out
+the files lying in these directories i.e. find(["LayoutTests"], set(["fast"]))
+will return everything except files in fast subfolder.
+
+If a callback is passed in, it will be called for the each file and the file
+will be included into the result if the callback returns True.
+The callback has to take three arguments: filesystem, dirname and filename."""
+
+
+def find(filesystem, base_dir, paths=None, skipped_directories=None, file_filter=None):
+    """Finds the set of tests under a given list of sub-paths.
+
+    Args:
+      paths: a list of path expressions relative to base_dir
+          to search. Glob patterns are ok, as are path expressions with
+          forward slashes on Windows. If paths is empty, we look at
+          everything under the base_dir.
+    """
+
+    paths = paths or ['*']
+    skipped_directories = skipped_directories or set(['.svn', '_svn'])
+    return _normalized_find(filesystem, _normalize(filesystem, base_dir, paths), skipped_directories, file_filter)
+
+
+def _normalize(filesystem, base_dir, paths):
+    return [filesystem.normpath(filesystem.join(base_dir, path)) for path in paths]
+
+
+def _normalized_find(filesystem, paths, skipped_directories, file_filter):
+    """Finds the set of tests under the list of paths.
+
+    Args:
+      paths: a list of absolute path expressions to search.
+          Glob patterns are ok.
+    """
+    paths_to_walk = set()
+
+    for path in paths:
+        # If there's an * in the name, assume it's a glob pattern.
+        if path.find('*') > -1:
+            filenames = filesystem.glob(path)
+            paths_to_walk.update(filenames)
+        else:
+            paths_to_walk.add(path)
+
+    # FIXME: I'm not sure there's much point in this being a set. A list would probably be faster.
+    all_files = set()
+    for path in paths_to_walk:
+        files = filesystem.files_under(path, skipped_directories, file_filter)
+        all_files.update(set(files))
+
+    return all_files
diff --git a/Tools/Scripts/webkitpy/common/find_files_unittest.py b/Tools/Scripts/webkitpy/common/find_files_unittest.py
new file mode 100644
index 0000000..75beaf0
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/find_files_unittest.py
@@ -0,0 +1,65 @@
+# Copyright (C) 2011 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 sys
+import unittest
+
+from webkitpy.common.system.filesystem import FileSystem
+import find_files
+
+
+class MockWinFileSystem(object):
+    def join(self, *paths):
+        return '\\'.join(paths)
+
+    def normpath(self, path):
+        return path.replace('/', '\\')
+
+
+class TestWinNormalize(unittest.TestCase):
+    def assert_filesystem_normalizes(self, filesystem):
+        self.assertEquals(find_files._normalize(filesystem, "c:\\foo",
+            ['fast/html', 'fast/canvas/*', 'compositing/foo.html']),
+            ['c:\\foo\\fast\html', 'c:\\foo\\fast\canvas\*', 'c:\\foo\compositing\\foo.html'])
+
+    def test_mocked_win(self):
+        # This tests test_files.normalize, using portable behavior emulating
+        # what we think Windows is supposed to do. This test will run on all
+        # platforms.
+        self.assert_filesystem_normalizes(MockWinFileSystem())
+
+    def test_win(self):
+        # This tests the actual windows platform, to ensure we get the same
+        # results that we get in test_mocked_win().
+        if sys.platform != 'win32':
+            return
+        self.assert_filesystem_normalizes(FileSystem())
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/common/host.py b/Tools/Scripts/webkitpy/common/host.py
new file mode 100644
index 0000000..7dd5ad0
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/host.py
@@ -0,0 +1,152 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+# Copyright (c) 2009 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:
+#
+#     * 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 logging
+import os
+import sys
+
+from webkitpy.common.checkout import Checkout
+from webkitpy.common.checkout.scm.detection import SCMDetector
+from webkitpy.common.memoized import memoized
+from webkitpy.common.net import bugzilla, buildbot, web
+from webkitpy.common.net.buildbot.chromiumbuildbot import ChromiumBuildBot
+from webkitpy.common.system.systemhost import SystemHost
+from webkitpy.common.watchlist.watchlistloader import WatchListLoader
+from webkitpy.layout_tests.port.factory import PortFactory
+
+
+_log = logging.getLogger(__name__)
+
+
+class Host(SystemHost):
+    def __init__(self):
+        SystemHost.__init__(self)
+        self.web = web.Web()
+
+        # FIXME: Checkout should own the scm object.
+        self._scm = None
+        self._checkout = None
+
+        # Everything below this line is WebKit-specific and belongs on a higher-level object.
+        self.bugs = bugzilla.Bugzilla()
+        self.buildbot = buildbot.BuildBot()
+
+        # FIXME: Unfortunately Port objects are currently the central-dispatch objects of the NRWT world.
+        # In order to instantiate a port correctly, we have to pass it at least an executive, user, scm, and filesystem
+        # so for now we just pass along the whole Host object.
+        # FIXME: PortFactory doesn't belong on this Host object if Port is going to have a Host (circular dependency).
+        self.port_factory = PortFactory(self)
+
+        self._engage_awesome_locale_hacks()
+
+    # We call this from the Host constructor, as it's one of the
+    # earliest calls made for all webkitpy-based programs.
+    def _engage_awesome_locale_hacks(self):
+        # To make life easier on our non-english users, we override
+        # the locale environment variables inside webkitpy.
+        # If we don't do this, programs like SVN will output localized
+        # messages and svn.py will fail to parse them.
+        # FIXME: We should do these overrides *only* for the subprocesses we know need them!
+        # This hack only works in unix environments.
+        os.environ['LANGUAGE'] = 'en'
+        os.environ['LANG'] = 'en_US.UTF-8'
+        os.environ['LC_MESSAGES'] = 'en_US.UTF-8'
+        os.environ['LC_ALL'] = ''
+
+    # FIXME: This is a horrible, horrible hack for ChromiumWin and should be removed.
+    # Maybe this belongs in SVN in some more generic "find the svn binary" codepath?
+    # Or possibly Executive should have a way to emulate shell path-lookups?
+    # FIXME: Unclear how to test this, since it currently mutates global state on SVN.
+    def _engage_awesome_windows_hacks(self):
+        try:
+            self.executive.run_command(['svn', 'help'])
+        except OSError, e:
+            try:
+                self.executive.run_command(['svn.bat', 'help'])
+                # Chromium Win uses the depot_tools package, which contains a number
+                # of development tools, including Python and svn. Instead of using a
+                # real svn executable, depot_tools indirects via a batch file, called
+                # svn.bat. This batch file allows depot_tools to auto-update the real
+                # svn executable, which is contained in a subdirectory.
+                #
+                # That's all fine and good, except that subprocess.popen can detect
+                # the difference between a real svn executable and batch file when we
+                # don't provide use shell=True. Rather than use shell=True on Windows,
+                # We hack the svn.bat name into the SVN class.
+                _log.debug('Engaging svn.bat Windows hack.')
+                from webkitpy.common.checkout.scm.svn import SVN
+                SVN.executable_name = 'svn.bat'
+            except OSError, e:
+                _log.debug('Failed to engage svn.bat Windows hack.')
+        try:
+            self.executive.run_command(['git', 'help'])
+        except OSError, e:
+            try:
+                self.executive.run_command(['git.bat', 'help'])
+                # Chromium Win uses the depot_tools package, which contains a number
+                # of development tools, including Python and git. Instead of using a
+                # real git executable, depot_tools indirects via a batch file, called
+                # git.bat. This batch file allows depot_tools to auto-update the real
+                # git executable, which is contained in a subdirectory.
+                #
+                # That's all fine and good, except that subprocess.popen can detect
+                # the difference between a real git executable and batch file when we
+                # don't provide use shell=True. Rather than use shell=True on Windows,
+                # We hack the git.bat name into the SVN class.
+                _log.debug('Engaging git.bat Windows hack.')
+                from webkitpy.common.checkout.scm.git import Git
+                Git.executable_name = 'git.bat'
+            except OSError, e:
+                _log.debug('Failed to engage git.bat Windows hack.')
+
+    def initialize_scm(self, patch_directories=None):
+        if sys.platform == "win32":
+            self._engage_awesome_windows_hacks()
+        detector = SCMDetector(self.filesystem, self.executive)
+        self._scm = detector.default_scm(patch_directories)
+        self._checkout = Checkout(self.scm())
+
+    def scm(self):
+        return self._scm
+
+    def checkout(self):
+        return self._checkout
+
+    def buildbot_for_builder_name(self, name):
+        if self.port_factory.get_from_builder_name(name).is_chromium():
+            return self.chromium_buildbot()
+        return self.buildbot
+
+    @memoized
+    def chromium_buildbot(self):
+        return ChromiumBuildBot()
+
+    @memoized
+    def watch_list(self):
+        return WatchListLoader(self.filesystem).load()
diff --git a/Tools/Scripts/webkitpy/common/host_mock.py b/Tools/Scripts/webkitpy/common/host_mock.py
new file mode 100644
index 0000000..8b508bf
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/host_mock.py
@@ -0,0 +1,81 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.checkout.checkout_mock import MockCheckout
+from webkitpy.common.checkout.scm.scm_mock import MockSCM
+from webkitpy.common.net.bugzilla.bugzilla_mock import MockBugzilla
+from webkitpy.common.net.buildbot.buildbot_mock import MockBuildBot
+from webkitpy.common.net.web_mock import MockWeb
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+from webkitpy.common.watchlist.watchlist_mock import MockWatchList
+
+# New-style ports need to move down into webkitpy.common.
+from webkitpy.layout_tests.port.factory import PortFactory
+from webkitpy.layout_tests.port.test import add_unit_tests_to_mock_filesystem
+
+
+class MockHost(MockSystemHost):
+    def __init__(self, log_executive=False, executive_throws_when_run=None, initialize_scm_by_default=True):
+        MockSystemHost.__init__(self, log_executive, executive_throws_when_run)
+        add_unit_tests_to_mock_filesystem(self.filesystem)
+        self.web = MockWeb()
+
+        self._checkout = MockCheckout()
+        self._scm = None
+        # FIXME: we should never initialize the SCM by default, since the real
+        # object doesn't either. This has caused at least one bug (see bug 89498).
+        if initialize_scm_by_default:
+            self.initialize_scm()
+        self.bugs = MockBugzilla()
+        self.buildbot = MockBuildBot()
+        self._chromium_buildbot = MockBuildBot()
+
+        # Note: We're using a real PortFactory here.  Tests which don't wish to depend
+        # on the list of known ports should override this with a MockPortFactory.
+        self.port_factory = PortFactory(self)
+
+        self._watch_list = MockWatchList()
+
+    def initialize_scm(self, patch_directories=None):
+        self._scm = MockSCM(filesystem=self.filesystem, executive=self.executive)
+        # Various pieces of code (wrongly) call filesystem.chdir(checkout_root).
+        # Making the checkout_root exist in the mock filesystem makes that chdir not raise.
+        self.filesystem.maybe_make_directory(self._scm.checkout_root)
+
+    def scm(self):
+        return self._scm
+
+    def checkout(self):
+        return self._checkout
+
+    def chromium_buildbot(self):
+        return self._chromium_buildbot
+
+    def watch_list(self):
+        return self._watch_list
+
diff --git a/Tools/Scripts/webkitpy/common/lru_cache.py b/Tools/Scripts/webkitpy/common/lru_cache.py
new file mode 100644
index 0000000..4178d0f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/lru_cache.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+class Node():
+    def __init__(self, key, value):
+        self.key = key
+        self.value = value
+        self.prev = None
+        self.next = None
+
+
+class LRUCache():
+    """An implementation of Least Recently Used (LRU) Cache."""
+
+    def __init__(self, capacity):
+        """Initializes a lru cache with the given capacity.
+
+        Args:
+            capacity: The capacity of the cache.
+        """
+        assert capacity > 0, "capacity (%s) must be greater than zero." % capacity
+        self._first = None
+        self._last = None
+        self._dict = {}
+        self._capacity = capacity
+
+    def __setitem__(self, key, value):
+        if key in self._dict:
+            self.__delitem__(key)
+        if not self._first:
+            self._one_node(key, value)
+            return
+        if len(self._dict) >= self._capacity:
+            del self._dict[self._last.key]
+            if self._capacity == 1:
+                self._one_node(key, value)
+                return
+            self._last = self._last.next
+            self._last.prev = None
+        node = Node(key, value)
+        node.prev = self._first
+        self._first.next = node
+        self._first = node
+        self._dict[key] = node
+
+    def _one_node(self, key, value):
+        node = Node(key, value)
+        self._dict[key] = node
+        self._first = node
+        self._last = node
+
+    def __getitem__(self, key):
+        if not self._first:
+            raise KeyError(str(key))
+        if self._first.key == key:
+            return self._first.value
+
+        if self._last.key == key:
+            next_last = self._last.next
+            next_last.prev = None
+            next_first = self._last
+            next_first.prev = self._first
+            next_first.next = None
+            self._first.next = next_first
+            self._first = next_first
+            self._last = next_last
+            return self._first.value
+
+        node = self._dict[key]
+        node.next.prev = node.prev
+        node.prev.next = node.next
+        node.prev = self._first
+        node.next = None
+        self._first.next = node
+        self._first = node
+        return self._first.value
+
+    def __delitem__(self, key):
+        node = self._dict[key]
+        del self._dict[key]
+        if self._first is self._last:
+            self._last = None
+            self._first = None
+            return
+        if self._first is node:
+            self._first = node.prev
+            self._first.next = None
+            return
+        if self._last is node:
+            self._last = node.next
+            self._last.prev = None
+            return
+        node.next.prev = node.prev
+        node.prev.next = node.next
+
+    def __len__(self):
+        return len(self._dict)
+
+    def __contains__(self, key):
+        return key in self._dict
+
+    def __iter__(self):
+        return iter(self._dict)
+
+    def items(self):
+        return [(key, node.value) for key, node in self._dict.items()]
+
+    def values(self):
+        return [node.value for node in self._dict.values()]
+
+    def keys(self):
+        return self._dict.keys()
diff --git a/Tools/Scripts/webkitpy/common/lru_cache_unittest.py b/Tools/Scripts/webkitpy/common/lru_cache_unittest.py
new file mode 100644
index 0000000..44a09e6
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/lru_cache_unittest.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+import unittest
+
+from webkitpy.common import lru_cache
+
+
+class LRUCacheTest(unittest.TestCase):
+
+    def setUp(self):
+        self.lru = lru_cache.LRUCache(3)
+        self.lru['key_1'] = 'item_1'
+        self.lru['key_2'] = 'item_2'
+        self.lru['key_3'] = 'item_3'
+
+        self.lru2 = lru_cache.LRUCache(1)
+        self.lru2['key_1'] = 'item_1'
+
+    def test_items(self):
+        self.assertEqual(set(self.lru.items()), set([('key_1', 'item_1'), ('key_3', 'item_3'), ('key_2', 'item_2')]))
+
+    def test_put(self):
+        self.lru['key_4'] = 'item_4'
+        self.assertEqual(set(self.lru.items()), set([('key_4', 'item_4'), ('key_3', 'item_3'), ('key_2', 'item_2')]))
+
+    def test_update(self):
+        self.lru['key_1']
+        self.lru['key_5'] = 'item_5'
+        self.assertEqual(set(self.lru.items()), set([('key_1', 'item_1'), ('key_3', 'item_3'), ('key_5', 'item_5')]))
+
+    def test_keys(self):
+        self.assertEqual(set(self.lru.keys()), set(['key_1', 'key_2', 'key_3']))
+
+    def test_delete(self):
+        del self.lru['key_1']
+        self.assertFalse('key_1' in self.lru)
+
+    def test_contain(self):
+        self.assertTrue('key_1' in self.lru)
+        self.assertFalse('key_4' in self.lru)
+
+    def test_values(self):
+        self.assertEqual(set(self.lru.values()), set(['item_1', 'item_2', 'item_3']))
+
+    def test_len(self):
+        self.assertEqual(len(self.lru), 3)
+
+    def test_size_one_pop(self):
+        self.lru2['key_2'] = 'item_2'
+        self.assertEqual(self.lru2.keys(), ['key_2'])
+
+    def test_size_one_delete(self):
+        del self.lru2['key_1']
+        self.assertFalse('key_1' in self.lru2)
+
+    def test_pop_error(self):
+        self.assertRaises(KeyError, self.lru2.__getitem__, 'key_2')
+        del self.lru2['key_1']
+        self.assertRaises(KeyError, self.lru2.__getitem__, 'key_2')
+
+    def test_get_middle_item(self):
+        self.lru['key_2']
+        self.lru['key_4'] = 'item_4'
+        self.lru['key_5'] = 'item_5'
+        self.assertEqual(set(self.lru.keys()), set(['key_2', 'key_4', 'key_5']))
+
+    def test_set_again(self):
+        self.lru['key_1'] = 'item_4'
+        self.assertEqual(set(self.lru.items()), set([('key_1', 'item_4'), ('key_3', 'item_3'), ('key_2', 'item_2')]))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/common/memoized.py b/Tools/Scripts/webkitpy/common/memoized.py
new file mode 100644
index 0000000..dc844a5
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/memoized.py
@@ -0,0 +1,55 @@
+# 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.
+
+# Python does not (yet) seem to provide automatic memoization.  So we've
+# written a small decorator to do so.
+
+import functools
+
+
+class memoized(object):
+    def __init__(self, function):
+        self._function = function
+        self._results_cache = {}
+
+    def __call__(self, *args):
+        try:
+            return self._results_cache[args]
+        except KeyError:
+            # If we didn't find the args in our cache, call and save the results.
+            result = self._function(*args)
+            self._results_cache[args] = result
+            return result
+        # FIXME: We may need to handle TypeError here in the case
+        # that "args" is not a valid dictionary key.
+
+    # Use python "descriptor" protocol __get__ to appear
+    # invisible during property access.
+    def __get__(self, instance, owner):
+        # Return a function partial with obj already bound as self.
+        return functools.partial(self.__call__, instance)
diff --git a/Tools/Scripts/webkitpy/common/memoized_unittest.py b/Tools/Scripts/webkitpy/common/memoized_unittest.py
new file mode 100644
index 0000000..dd7c793
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/memoized_unittest.py
@@ -0,0 +1,65 @@
+# 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.memoized import memoized
+
+
+class _TestObject(object):
+    def __init__(self):
+        self.callCount = 0
+
+    @memoized
+    def memoized_add(self, argument):
+        """testing docstring"""
+        self.callCount += 1
+        if argument is None:
+            return None  # Avoid the TypeError from None + 1
+        return argument + 1
+
+
+class MemoizedTest(unittest.TestCase):
+    def test_caching(self):
+        test = _TestObject()
+        test.callCount = 0
+        self.assertEqual(test.memoized_add(1), 2)
+        self.assertEqual(test.callCount, 1)
+        self.assertEqual(test.memoized_add(1), 2)
+        self.assertEqual(test.callCount, 1)
+
+        # Validate that callCount is working as expected.
+        self.assertEqual(test.memoized_add(2), 3)
+        self.assertEqual(test.callCount, 2)
+
+    def test_tearoff(self):
+        test = _TestObject()
+        # Make sure that get()/tear-offs work:
+        tearoff = test.memoized_add
+        self.assertEqual(tearoff(4), 5)
+        self.assertEqual(test.callCount, 1)
diff --git a/Tools/Scripts/webkitpy/common/message_pool.py b/Tools/Scripts/webkitpy/common/message_pool.py
new file mode 100644
index 0000000..2e1e85e
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/message_pool.py
@@ -0,0 +1,324 @@
+# Copyright (C) 2011 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.
+
+"""Module for handling messages and concurrency for run-webkit-tests
+and test-webkitpy. This module follows the design for multiprocessing.Pool
+and concurrency.futures.ProcessPoolExecutor, with the following differences:
+
+* Tasks are executed in stateful subprocesses via objects that implement the
+  Worker interface - this allows the workers to share state across tasks.
+* The pool provides an asynchronous event-handling interface so the caller
+  may receive events as tasks are processed.
+
+If you don't need these features, use multiprocessing.Pool or concurrency.futures
+intead.
+
+"""
+
+import cPickle
+import logging
+import multiprocessing
+import Queue
+import sys
+import time
+import traceback
+
+
+from webkitpy.common.host import Host
+from webkitpy.common.system import stack_utils
+
+
+_log = logging.getLogger(__name__)
+
+
+def get(caller, worker_factory, num_workers, worker_startup_delay_secs=0.0, host=None):
+    """Returns an object that exposes a run() method that takes a list of test shards and runs them in parallel."""
+    return _MessagePool(caller, worker_factory, num_workers, worker_startup_delay_secs, host)
+
+
+class _MessagePool(object):
+    def __init__(self, caller, worker_factory, num_workers, worker_startup_delay_secs=0.0, host=None):
+        self._caller = caller
+        self._worker_factory = worker_factory
+        self._num_workers = num_workers
+        self._worker_startup_delay_secs = worker_startup_delay_secs
+        self._workers = []
+        self._workers_stopped = set()
+        self._host = host
+        self._name = 'manager'
+        self._running_inline = (self._num_workers == 1)
+        if self._running_inline:
+            self._messages_to_worker = Queue.Queue()
+            self._messages_to_manager = Queue.Queue()
+        else:
+            self._messages_to_worker = multiprocessing.Queue()
+            self._messages_to_manager = multiprocessing.Queue()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, exc_traceback):
+        self._close()
+        return False
+
+    def run(self, shards):
+        """Posts a list of messages to the pool and waits for them to complete."""
+        for message in shards:
+            self._messages_to_worker.put(_Message(self._name, message[0], message[1:], from_user=True, logs=()))
+
+        for _ in xrange(self._num_workers):
+            self._messages_to_worker.put(_Message(self._name, 'stop', message_args=(), from_user=False, logs=()))
+
+        self.wait()
+
+    def _start_workers(self):
+        assert not self._workers
+        self._workers_stopped = set()
+        host = None
+        if self._running_inline or self._can_pickle(self._host):
+            host = self._host
+
+        for worker_number in xrange(self._num_workers):
+            worker = _Worker(host, self._messages_to_manager, self._messages_to_worker, self._worker_factory, worker_number, self._running_inline, self if self._running_inline else None, self._worker_log_level())
+            self._workers.append(worker)
+            worker.start()
+            if self._worker_startup_delay_secs:
+                time.sleep(self._worker_startup_delay_secs)
+
+    def _worker_log_level(self):
+        log_level = logging.NOTSET
+        for handler in logging.root.handlers:
+            if handler.level != logging.NOTSET:
+                if log_level == logging.NOTSET:
+                    log_level = handler.level
+                else:
+                    log_level = min(log_level, handler.level)
+        return log_level
+
+    def wait(self):
+        try:
+            self._start_workers()
+            if self._running_inline:
+                self._workers[0].run()
+                self._loop(block=False)
+            else:
+                self._loop(block=True)
+        finally:
+            self._close()
+
+    def _close(self):
+        for worker in self._workers:
+            if worker.is_alive():
+                worker.terminate()
+                worker.join()
+        self._workers = []
+        if not self._running_inline:
+            # FIXME: This is a hack to get multiprocessing to not log tracebacks during shutdown :(.
+            multiprocessing.util._exiting = True
+            if self._messages_to_worker:
+                self._messages_to_worker.close()
+                self._messages_to_worker = None
+            if self._messages_to_manager:
+                self._messages_to_manager.close()
+                self._messages_to_manager = None
+
+    def _log_messages(self, messages):
+        for message in messages:
+            logging.root.handle(message)
+
+    def _handle_done(self, source):
+        self._workers_stopped.add(source)
+
+    @staticmethod
+    def _handle_worker_exception(source, exception_type, exception_value, _):
+        if exception_type == KeyboardInterrupt:
+            raise exception_type(exception_value)
+        raise WorkerException(str(exception_value))
+
+    def _can_pickle(self, host):
+        try:
+            cPickle.dumps(host)
+            return True
+        except TypeError:
+            return False
+
+    def _loop(self, block):
+        try:
+            while True:
+                if len(self._workers_stopped) == len(self._workers):
+                    block = False
+                message = self._messages_to_manager.get(block)
+                self._log_messages(message.logs)
+                if message.from_user:
+                    self._caller.handle(message.name, message.src, *message.args)
+                    continue
+                method = getattr(self, '_handle_' + message.name)
+                assert method, 'bad message %s' % repr(message)
+                method(message.src, *message.args)
+        except Queue.Empty:
+            pass
+
+
+class WorkerException(Exception):
+    """Raised when we receive an unexpected/unknown exception from a worker."""
+    pass
+
+
+class _Message(object):
+    def __init__(self, src, message_name, message_args, from_user, logs):
+        self.src = src
+        self.name = message_name
+        self.args = message_args
+        self.from_user = from_user
+        self.logs = logs
+
+    def __repr__(self):
+        return '_Message(src=%s, name=%s, args=%s, from_user=%s, logs=%s)' % (self.src, self.name, self.args, self.from_user, self.logs)
+
+
+class _Worker(multiprocessing.Process):
+    def __init__(self, host, messages_to_manager, messages_to_worker, worker_factory, worker_number, running_inline, manager, log_level):
+        super(_Worker, self).__init__()
+        self.host = host
+        self.worker_number = worker_number
+        self.name = 'worker/%d' % worker_number
+        self.log_messages = []
+        self.log_level = log_level
+        self._running_inline = running_inline
+        self._manager = manager
+
+        self._messages_to_manager = messages_to_manager
+        self._messages_to_worker = messages_to_worker
+        self._worker = worker_factory(self)
+        self._logger = None
+        self._log_handler = None
+
+    def terminate(self):
+        if self._worker:
+            if hasattr(self._worker, 'stop'):
+                self._worker.stop()
+            self._worker = None
+        if self.is_alive():
+            super(_Worker, self).terminate()
+
+    def _close(self):
+        if self._log_handler and self._logger:
+            self._logger.removeHandler(self._log_handler)
+        self._log_handler = None
+        self._logger = None
+
+    def start(self):
+        if not self._running_inline:
+            super(_Worker, self).start()
+
+    def run(self):
+        if not self.host:
+            self.host = Host()
+        if not self._running_inline:
+            self._set_up_logging()
+
+        worker = self._worker
+        exception_msg = ""
+        _log.debug("%s starting" % self.name)
+
+        try:
+            if hasattr(worker, 'start'):
+                worker.start()
+            while True:
+                message = self._messages_to_worker.get()
+                if message.from_user:
+                    worker.handle(message.name, message.src, *message.args)
+                    self._yield_to_manager()
+                else:
+                    assert message.name == 'stop', 'bad message %s' % repr(message)
+                    break
+
+            _log.debug("%s exiting" % self.name)
+        except Queue.Empty:
+            assert False, '%s: ran out of messages in worker queue.' % self.name
+        except KeyboardInterrupt, e:
+            self._raise(sys.exc_info())
+        except Exception, e:
+            self._raise(sys.exc_info())
+        finally:
+            try:
+                if hasattr(worker, 'stop'):
+                    worker.stop()
+            finally:
+                self._post(name='done', args=(), from_user=False)
+            self._close()
+
+    def post(self, name, *args):
+        self._post(name, args, from_user=True)
+        self._yield_to_manager()
+
+    def _yield_to_manager(self):
+        if self._running_inline:
+            self._manager._loop(block=False)
+
+    def _post(self, name, args, from_user):
+        log_messages = self.log_messages
+        self.log_messages = []
+        self._messages_to_manager.put(_Message(self.name, name, args, from_user, log_messages))
+
+    def _raise(self, exc_info):
+        exception_type, exception_value, exception_traceback = exc_info
+        if self._running_inline:
+            raise exception_type, exception_value, exception_traceback
+
+        if exception_type == KeyboardInterrupt:
+            _log.debug("%s: interrupted, exiting" % self.name)
+            stack_utils.log_traceback(_log.debug, exception_traceback)
+        else:
+            _log.error("%s: %s('%s') raised:" % (self.name, exception_value.__class__.__name__, str(exception_value)))
+            stack_utils.log_traceback(_log.error, exception_traceback)
+        # Since tracebacks aren't picklable, send the extracted stack instead.
+        stack = traceback.extract_tb(exception_traceback)
+        self._post(name='worker_exception', args=(exception_type, exception_value, stack), from_user=False)
+
+    def _set_up_logging(self):
+        self._logger = logging.getLogger()
+
+        # The unix multiprocessing implementation clones any log handlers into the child process,
+        # so we remove them to avoid duplicate logging.
+        for h in self._logger.handlers:
+            self._logger.removeHandler(h)
+
+        self._log_handler = _WorkerLogHandler(self)
+        self._logger.addHandler(self._log_handler)
+        self._logger.setLevel(self.log_level)
+
+
+class _WorkerLogHandler(logging.Handler):
+    def __init__(self, worker):
+        logging.Handler.__init__(self)
+        self._worker = worker
+        self.setLevel(worker.log_level)
+
+    def emit(self, record):
+        self._worker.log_messages.append(record)
diff --git a/Tools/Scripts/webkitpy/common/multiprocessing_bootstrap.py b/Tools/Scripts/webkitpy/common/multiprocessing_bootstrap.py
new file mode 100755
index 0000000..1189776
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/multiprocessing_bootstrap.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# Copyright (c) 2011 Google Inc. All rights reserved.
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+"""In order for the multiprocessing module to spawn children correctly on
+Windows, we need to be running a Python module that can be imported
+(which means a file in sys.path that ends in .py). In addition, we need to
+ensure that sys.path / PYTHONPATH is set and propagating correctly.
+
+This module enforces that."""
+
+import os
+import subprocess
+import sys
+
+from webkitpy.common import version_check   # 'unused import' pylint: disable=W0611
+
+
+def run(*parts):
+    up = os.path.dirname
+    script_dir = up(up(up(os.path.abspath(__file__))))
+    env = os.environ
+    if 'PYTHONPATH' in env:
+        if script_dir not in env['PYTHONPATH']:
+            env['PYTHONPATH'] = env['PYTHONPATH'] + os.pathsep + script_dir
+    else:
+        env['PYTHONPATH'] = script_dir
+    module_path = os.path.join(script_dir, *parts)
+    cmd = [sys.executable, module_path] + sys.argv[1:]
+
+    # Wrap processes in the jhbuild environment so DRT or WKTR
+    # doesn't need to do it and their process id as reported by
+    # subprocess.Popen is not jhbuild's.
+    if '--gtk' in sys.argv[1:] and os.path.exists(os.path.join(script_dir, '..', '..', 'WebKitBuild', 'Dependencies')):
+        cmd.insert(1, os.path.join(script_dir, '..', 'gtk', 'run-with-jhbuild'))
+
+    proc = subprocess.Popen(cmd, env=env)
+    try:
+        proc.wait()
+    except KeyboardInterrupt:
+        # We need a second wait in order to make sure the subprocess exits fully.
+        # FIXME: It would be nice if we could put a timeout on this.
+        proc.wait()
+    sys.exit(proc.returncode)
diff --git a/Tools/Scripts/webkitpy/common/net/__init__.py b/Tools/Scripts/webkitpy/common/net/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/common/net/bugzilla/__init__.py b/Tools/Scripts/webkitpy/common/net/bugzilla/__init__.py
new file mode 100644
index 0000000..c427b18
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/bugzilla/__init__.py
@@ -0,0 +1,7 @@
+# Required for Python to search this directory for module files
+
+# We only export public API here.
+from .bugzilla import Bugzilla
+# Unclear if Bug and Attachment need to be public classes.
+from .bug import Bug
+from .attachment import Attachment
diff --git a/Tools/Scripts/webkitpy/common/net/bugzilla/attachment.py b/Tools/Scripts/webkitpy/common/net/bugzilla/attachment.py
new file mode 100644
index 0000000..6e10d65
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/bugzilla/attachment.py
@@ -0,0 +1,118 @@
+# Copyright (c) 2009 Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+# Copyright (c) 2010 Research In Motion Limited. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.memoized import memoized
+from webkitpy.common.system.deprecated_logging import log
+
+
+class Attachment(object):
+
+    rollout_preamble = "ROLLOUT of r"
+
+    def __init__(self, attachment_dictionary, bug):
+        self._attachment_dictionary = attachment_dictionary
+        self._bug = bug
+        # FIXME: These should be replaced with @memoized after updating mocks.
+        self._reviewer = None
+        self._committer = None
+
+    def _bugzilla(self):
+        return self._bug._bugzilla
+
+    def id(self):
+        return int(self._attachment_dictionary.get("id"))
+
+    @memoized
+    def attacher(self):
+        return self._bugzilla().committers.contributor_by_email(self.attacher_email())
+
+    def attacher_email(self):
+        return self._attachment_dictionary.get("attacher_email")
+
+    def bug(self):
+        return self._bug
+
+    def bug_id(self):
+        return int(self._attachment_dictionary.get("bug_id"))
+
+    def is_patch(self):
+        return not not self._attachment_dictionary.get("is_patch")
+
+    def is_obsolete(self):
+        return not not self._attachment_dictionary.get("is_obsolete")
+
+    def is_rollout(self):
+        return self.name().startswith(self.rollout_preamble)
+
+    def name(self):
+        return self._attachment_dictionary.get("name")
+
+    def attach_date(self):
+        return self._attachment_dictionary.get("attach_date")
+
+    def review(self):
+        return self._attachment_dictionary.get("review")
+
+    def commit_queue(self):
+        return self._attachment_dictionary.get("commit-queue")
+
+    def url(self):
+        # FIXME: This should just return
+        # self._bugzilla().attachment_url_for_id(self.id()). scm_unittest.py
+        # depends on the current behavior.
+        return self._attachment_dictionary.get("url")
+
+    def contents(self):
+        # FIXME: We shouldn't be grabbing at _bugzilla.
+        return self._bug._bugzilla.fetch_attachment_contents(self.id())
+
+    def _validate_flag_value(self, flag):
+        email = self._attachment_dictionary.get("%s_email" % flag)
+        if not email:
+            return None
+        # FIXME: This is not a robust way to call committer_by_email
+        committer = getattr(self._bugzilla().committers,
+                            "%s_by_email" % flag)(email)
+        if committer:
+            return committer
+        log("Warning, attachment %s on bug %s has invalid %s (%s)" % (
+                 self._attachment_dictionary['id'],
+                 self._attachment_dictionary['bug_id'], flag, email))
+
+    # FIXME: These could use @memoized like attacher(), but unit tests would need updates.
+    def reviewer(self):
+        if not self._reviewer:
+            self._reviewer = self._validate_flag_value("reviewer")
+        return self._reviewer
+
+    def committer(self):
+        if not self._committer:
+            self._committer = self._validate_flag_value("committer")
+        return self._committer
diff --git a/Tools/Scripts/webkitpy/common/net/bugzilla/bug.py b/Tools/Scripts/webkitpy/common/net/bugzilla/bug.py
new file mode 100644
index 0000000..4bf8ec6
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/bugzilla/bug.py
@@ -0,0 +1,125 @@
+# Copyright (c) 2009 Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+# Copyright (c) 2010 Research In Motion Limited. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from .attachment import Attachment
+
+
+class Bug(object):
+    # FIXME: This class is kinda a hack for now.  It exists so we have one
+    # place to hold bug logic, even if much of the code deals with
+    # dictionaries still.
+
+    def __init__(self, bug_dictionary, bugzilla):
+        self.bug_dictionary = bug_dictionary
+        self._bugzilla = bugzilla
+
+    def id(self):
+        return self.bug_dictionary["id"]
+
+    def title(self):
+        # FIXME: Do we need to HTML unescape the title?
+        return self.bug_dictionary["title"]
+
+    def reporter_email(self):
+        return self.bug_dictionary["reporter_email"]
+
+    def assigned_to_email(self):
+        return self.bug_dictionary["assigned_to_email"]
+
+    def cc_emails(self):
+        return self.bug_dictionary["cc_emails"]
+
+    # FIXME: This information should be stored in some sort of webkit_config.py instead of here.
+    unassigned_emails = frozenset([
+        "webkit-unassigned@lists.webkit.org",
+        "webkit-qt-unassigned@trolltech.com",
+    ])
+
+    def is_unassigned(self):
+        return self.assigned_to_email() in self.unassigned_emails
+
+    def status(self):
+        return self.bug_dictionary["bug_status"]
+
+    # Bugzilla has many status states we don't really use in WebKit:
+    # https://bugs.webkit.org/page.cgi?id=fields.html#status
+    _open_states = ["UNCONFIRMED", "NEW", "ASSIGNED", "REOPENED"]
+    _closed_states = ["RESOLVED", "VERIFIED", "CLOSED"]
+
+    def is_open(self):
+        return self.status() in self._open_states
+
+    def is_closed(self):
+        return not self.is_open()
+
+    def duplicate_of(self):
+        return self.bug_dictionary.get('dup_id', None)
+
+    # Rarely do we actually want obsolete attachments
+    def attachments(self, include_obsolete=False):
+        attachments = self.bug_dictionary["attachments"]
+        if not include_obsolete:
+            attachments = filter(lambda attachment:
+                                 not attachment["is_obsolete"], attachments)
+        return [Attachment(attachment, self) for attachment in attachments]
+
+    def patches(self, include_obsolete=False):
+        return [patch for patch in self.attachments(include_obsolete)
+                                   if patch.is_patch()]
+
+    def unreviewed_patches(self):
+        return [patch for patch in self.patches() if patch.review() == "?"]
+
+    def reviewed_patches(self, include_invalid=False):
+        patches = [patch for patch in self.patches() if patch.review() == "+"]
+        if include_invalid:
+            return patches
+        # Checking reviewer() ensures that it was both reviewed and has a valid
+        # reviewer.
+        return filter(lambda patch: patch.reviewer(), patches)
+
+    def commit_queued_patches(self, include_invalid=False):
+        patches = [patch for patch in self.patches()
+                                      if patch.commit_queue() == "+"]
+        if include_invalid:
+            return patches
+        # Checking committer() ensures that it was both commit-queue+'d and has
+        # a valid committer.
+        return filter(lambda patch: patch.committer(), patches)
+
+    def comments(self):
+        return self.bug_dictionary["comments"]
+
+    def is_in_comments(self, message):
+        for comment in self.comments():
+            if message in comment["text"]:
+                return True
+        return False
+
diff --git a/Tools/Scripts/webkitpy/common/net/bugzilla/bug_unittest.py b/Tools/Scripts/webkitpy/common/net/bugzilla/bug_unittest.py
new file mode 100644
index 0000000..f20c601
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/bugzilla/bug_unittest.py
@@ -0,0 +1,47 @@
+# Copyright (C) 2009 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 .bug import Bug
+
+
+class BugTest(unittest.TestCase):
+    def test_is_unassigned(self):
+        for email in Bug.unassigned_emails:
+            bug = Bug({"assigned_to_email": email}, bugzilla=None)
+            self.assertTrue(bug.is_unassigned())
+        bug = Bug({"assigned_to_email": "test@test.com"}, bugzilla=None)
+        self.assertFalse(bug.is_unassigned())
+
+    def test_is_in_comments(self):
+        bug = Bug({"comments": [{"text": "Message1."},
+                                {"text": "Message2. Message3. Message4."}, ], },
+                  bugzilla=None)
+        self.assertTrue(bug.is_in_comments("Message3."))
+        self.assertFalse(bug.is_in_comments("Message."))
diff --git a/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla.py b/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla.py
new file mode 100644
index 0000000..651e1b3
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla.py
@@ -0,0 +1,856 @@
+# Copyright (c) 2011 Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+# Copyright (c) 2010 Research In Motion Limited. 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.
+#
+# WebKit's Python module for interacting with Bugzilla
+
+import mimetypes
+import re
+import StringIO
+import socket
+import urllib
+
+from datetime import datetime # used in timestamp()
+
+from .attachment import Attachment
+from .bug import Bug
+
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.common.config import committers
+import webkitpy.common.config.urls as config_urls
+from webkitpy.common.net.credentials import Credentials
+from webkitpy.common.system.user import User
+from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, SoupStrainer
+
+
+class EditUsersParser(object):
+    def __init__(self):
+        self._group_name_to_group_string_cache = {}
+
+    def _login_and_uid_from_row(self, row):
+        first_cell = row.find("td")
+        # The first row is just headers, we skip it.
+        if not first_cell:
+            return None
+        # When there were no results, we have a fake "<none>" entry in the table.
+        if first_cell.find(text="<none>"):
+            return None
+        # Otherwise the <td> contains a single <a> which contains the login name or a single <i> with the string "<none>".
+        anchor_tag = first_cell.find("a")
+        login = unicode(anchor_tag.string).strip()
+        user_id = int(re.search(r"userid=(\d+)", str(anchor_tag['href'])).group(1))
+        return (login, user_id)
+
+    def login_userid_pairs_from_edit_user_results(self, results_page):
+        soup = BeautifulSoup(results_page, convertEntities=BeautifulStoneSoup.HTML_ENTITIES)
+        results_table = soup.find(id="admin_table")
+        login_userid_pairs = [self._login_and_uid_from_row(row) for row in results_table('tr')]
+        # Filter out None from the logins.
+        return filter(lambda pair: bool(pair), login_userid_pairs)
+
+    def _group_name_and_string_from_row(self, row):
+        label_element = row.find('label')
+        group_string = unicode(label_element['for'])
+        group_name = unicode(label_element.find('strong').string).rstrip(':')
+        return (group_name, group_string)
+
+    def user_dict_from_edit_user_page(self, page):
+        soup = BeautifulSoup(page, convertEntities=BeautifulStoneSoup.HTML_ENTITIES)
+        user_table = soup.find("table", {'class': 'main'})
+        user_dict = {}
+        for row in user_table('tr'):
+            label_element = row.find('label')
+            if not label_element:
+                continue  # This must not be a row we know how to parse.
+            if row.find('table'):
+                continue  # Skip the <tr> holding the groups table.
+
+            key = label_element['for']
+            if "group" in key:
+                key = "groups"
+                value = user_dict.get('groups', set())
+                # We must be parsing a "tr" inside the inner group table.
+                (group_name, _) = self._group_name_and_string_from_row(row)
+                if row.find('input', {'type': 'checkbox', 'checked': 'checked'}):
+                    value.add(group_name)
+            else:
+                value = unicode(row.find('td').string).strip()
+            user_dict[key] = value
+        return user_dict
+
+    def _group_rows_from_edit_user_page(self, edit_user_page):
+        soup = BeautifulSoup(edit_user_page, convertEntities=BeautifulSoup.HTML_ENTITIES)
+        return soup('td', {'class': 'groupname'})
+
+    def group_string_from_name(self, edit_user_page, group_name):
+        # Bugzilla uses "group_NUMBER" strings, which may be different per install
+        # so we just look them up once and cache them.
+        if not self._group_name_to_group_string_cache:
+            rows = self._group_rows_from_edit_user_page(edit_user_page)
+            name_string_pairs = map(self._group_name_and_string_from_row, rows)
+            self._group_name_to_group_string_cache = dict(name_string_pairs)
+        return self._group_name_to_group_string_cache[group_name]
+
+
+def timestamp():
+    return datetime.now().strftime("%Y%m%d%H%M%S")
+
+
+# A container for all of the logic for making and parsing bugzilla queries.
+class BugzillaQueries(object):
+
+    def __init__(self, bugzilla):
+        self._bugzilla = bugzilla
+
+    def _is_xml_bugs_form(self, form):
+        # ClientForm.HTMLForm.find_control throws if the control is not found,
+        # so we do a manual search instead:
+        return "xml" in [control.id for control in form.controls]
+
+    # This is kinda a hack.  There is probably a better way to get this information from bugzilla.
+    def _parse_result_count(self, results_page):
+        result_count_text = BeautifulSoup(results_page).find(attrs={'class': 'bz_result_count'}).string
+        result_count_parts = result_count_text.strip().split(" ")
+        if result_count_parts[0] == "Zarro":
+            return 0
+        if result_count_parts[0] == "One":
+            return 1
+        return int(result_count_parts[0])
+
+    # Note: _load_query, _fetch_bug and _fetch_bugs_from_advanced_query
+    # are the only methods which access self._bugzilla.
+
+    def _load_query(self, query):
+        self._bugzilla.authenticate()
+        full_url = "%s%s" % (config_urls.bug_server_url, query)
+        return self._bugzilla.browser.open(full_url)
+
+    def _fetch_bugs_from_advanced_query(self, query):
+        results_page = self._load_query(query)
+        # Some simple searches can return a single result.
+        results_url = results_page.geturl()
+        if results_url.find("/show_bug.cgi?id=") != -1:
+            bug_id = int(results_url.split("=")[-1])
+            return [self._fetch_bug(bug_id)]
+        if not self._parse_result_count(results_page):
+            return []
+        # Bugzilla results pages have an "XML" submit button at the bottom
+        # which can be used to get an XML page containing all of the <bug> elements.
+        # This is slighty lame that this assumes that _load_query used
+        # self._bugzilla.browser and that it's in an acceptable state.
+        self._bugzilla.browser.select_form(predicate=self._is_xml_bugs_form)
+        bugs_xml = self._bugzilla.browser.submit()
+        return self._bugzilla._parse_bugs_from_xml(bugs_xml)
+
+    def _fetch_bug(self, bug_id):
+        return self._bugzilla.fetch_bug(bug_id)
+
+    def _fetch_bug_ids_advanced_query(self, query):
+        soup = BeautifulSoup(self._load_query(query))
+        # The contents of the <a> inside the cells in the first column happen
+        # to be the bug id.
+        return [int(bug_link_cell.find("a").string)
+                for bug_link_cell in soup('td', "first-child")]
+
+    def _parse_attachment_ids_request_query(self, page):
+        digits = re.compile("\d+")
+        attachment_href = re.compile("attachment.cgi\?id=\d+&action=review")
+        attachment_links = SoupStrainer("a", href=attachment_href)
+        return [int(digits.search(tag["href"]).group(0))
+                for tag in BeautifulSoup(page, parseOnlyThese=attachment_links)]
+
+    def _fetch_attachment_ids_request_query(self, query):
+        return self._parse_attachment_ids_request_query(self._load_query(query))
+
+    def _parse_quips(self, page):
+        soup = BeautifulSoup(page, convertEntities=BeautifulSoup.HTML_ENTITIES)
+        quips = soup.find(text=re.compile(r"Existing quips:")).findNext("ul").findAll("li")
+        return [unicode(quip_entry.string) for quip_entry in quips]
+
+    def fetch_quips(self):
+        return self._parse_quips(self._load_query("/quips.cgi?action=show"))
+
+    # List of all r+'d bugs.
+    def fetch_bug_ids_from_pending_commit_list(self):
+        needs_commit_query_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%2B"
+        return self._fetch_bug_ids_advanced_query(needs_commit_query_url)
+
+    def fetch_bugs_matching_quicksearch(self, search_string):
+        # We may want to use a more explicit query than "quicksearch".
+        # If quicksearch changes we should probably change to use
+        # a normal buglist.cgi?query_format=advanced query.
+        quicksearch_url = "buglist.cgi?quicksearch=%s" % urllib.quote(search_string)
+        return self._fetch_bugs_from_advanced_query(quicksearch_url)
+
+    # Currently this returns all bugs across all components.
+    # In the future we may wish to extend this API to construct more restricted searches.
+    def fetch_bugs_matching_search(self, search_string):
+        query = "buglist.cgi?query_format=advanced"
+        if search_string:
+            query += "&short_desc_type=allwordssubstr&short_desc=%s" % urllib.quote(search_string)
+        return self._fetch_bugs_from_advanced_query(query)
+
+    def fetch_patches_from_pending_commit_list(self):
+        return sum([self._fetch_bug(bug_id).reviewed_patches()
+            for bug_id in self.fetch_bug_ids_from_pending_commit_list()], [])
+
+    def fetch_bugs_from_review_queue(self, cc_email=None):
+        query = "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?"
+
+        if cc_email:
+            query += "&emailcc1=1&emailtype1=substring&email1=%s" % urllib.quote(cc_email)
+
+        return self._fetch_bugs_from_advanced_query(query)
+
+    def fetch_bug_ids_from_commit_queue(self):
+        commit_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=commit-queue%2B&order=Last+Changed"
+        return self._fetch_bug_ids_advanced_query(commit_queue_url)
+
+    def fetch_patches_from_commit_queue(self):
+        # This function will only return patches which have valid committers
+        # set.  It won't reject patches with invalid committers/reviewers.
+        return sum([self._fetch_bug(bug_id).commit_queued_patches()
+                    for bug_id in self.fetch_bug_ids_from_commit_queue()], [])
+
+    def fetch_bug_ids_from_review_queue(self):
+        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]], [])
+
+    # 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)
+
+    # This only works if your account has edituser privileges.
+    # We could easily parse https://bugs.webkit.org/userprefs.cgi?tab=permissions to
+    # check permissions, but bugzilla will just return an error if we don't have them.
+    def fetch_login_userid_pairs_matching_substring(self, search_string):
+        review_queue_url = "editusers.cgi?action=list&matchvalue=login_name&matchstr=%s&matchtype=substr" % urllib.quote(search_string)
+        results_page = self._load_query(review_queue_url)
+        # We could pull the EditUsersParser off Bugzilla if needed.
+        return EditUsersParser().login_userid_pairs_from_edit_user_results(results_page)
+
+    # FIXME: We should consider adding a BugzillaUser class.
+    def fetch_logins_matching_substring(self, search_string):
+        pairs = self.fetch_login_userid_pairs_matching_substring(search_string)
+        return map(lambda pair: pair[0], pairs)
+
+
+class Bugzilla(object):
+    def __init__(self, committers=committers.CommitterList()):
+        self.authenticated = False
+        self.queries = BugzillaQueries(self)
+        self.committers = committers
+        self.cached_quips = []
+        self.edit_user_parser = EditUsersParser()
+        self._browser = None
+
+    def _get_browser(self):
+        if not self._browser:
+            self.setdefaulttimeout(600)
+            from webkitpy.thirdparty.autoinstalled.mechanize import Browser
+            self._browser = Browser()
+            # Ignore bugs.webkit.org/robots.txt until we fix it to allow this script.
+            self._browser.set_handle_robots(False)
+        return self._browser
+
+    def _set_browser(self, value):
+        self._browser = value
+
+    browser = property(_get_browser, _set_browser)
+
+    def setdefaulttimeout(self, value):
+        socket.setdefaulttimeout(value)
+
+    def fetch_user(self, user_id):
+        self.authenticate()
+        edit_user_page = self.browser.open(self.edit_user_url_for_id(user_id))
+        return self.edit_user_parser.user_dict_from_edit_user_page(edit_user_page)
+
+    def add_user_to_groups(self, user_id, group_names):
+        self.authenticate()
+        user_edit_page = self.browser.open(self.edit_user_url_for_id(user_id))
+        self.browser.select_form(nr=1)
+        for group_name in group_names:
+            group_string = self.edit_user_parser.group_string_from_name(user_edit_page, group_name)
+            self.browser.find_control(group_string).items[0].selected = True
+        self.browser.submit()
+
+    def quips(self):
+        # We only fetch and parse the list of quips once per instantiation
+        # so that we do not burden bugs.webkit.org.
+        if not self.cached_quips:
+            self.cached_quips = self.queries.fetch_quips()
+        return self.cached_quips
+
+    def bug_url_for_bug_id(self, bug_id, xml=False):
+        if not bug_id:
+            return None
+        content_type = "&ctype=xml&excludefield=attachmentdata" if xml else ""
+        return "%sshow_bug.cgi?id=%s%s" % (config_urls.bug_server_url, bug_id, content_type)
+
+    def short_bug_url_for_bug_id(self, bug_id):
+        if not bug_id:
+            return None
+        return "http://webkit.org/b/%s" % bug_id
+
+    def add_attachment_url(self, bug_id):
+        return "%sattachment.cgi?action=enter&bugid=%s" % (config_urls.bug_server_url, bug_id)
+
+    def attachment_url_for_id(self, attachment_id, action="view"):
+        if not attachment_id:
+            return None
+        action_param = ""
+        if action and action != "view":
+            action_param = "&action=%s" % action
+        return "%sattachment.cgi?id=%s%s" % (config_urls.bug_server_url,
+                                             attachment_id,
+                                             action_param)
+
+    def edit_user_url_for_id(self, user_id):
+        return "%seditusers.cgi?action=edit&userid=%s" % (config_urls.bug_server_url, user_id)
+
+    def _parse_attachment_flag(self,
+                               element,
+                               flag_name,
+                               attachment,
+                               result_key):
+        flag = element.find('flag', attrs={'name': flag_name})
+        if flag:
+            attachment[flag_name] = flag['status']
+            if flag['status'] == '+':
+                attachment[result_key] = flag['setter']
+        # Sadly show_bug.cgi?ctype=xml does not expose the flag modification date.
+
+    def _string_contents(self, soup):
+        # WebKit's bugzilla instance uses UTF-8.
+        # BeautifulStoneSoup always returns Unicode strings, however
+        # the .string method returns a (unicode) NavigableString.
+        # NavigableString can confuse other parts of the code, so we
+        # convert from NavigableString to a real unicode() object using unicode().
+        return unicode(soup.string)
+
+    # Example: 2010-01-20 14:31 PST
+    # FIXME: Some bugzilla dates seem to have seconds in them?
+    # Python does not support timezones out of the box.
+    # Assume that bugzilla always uses PST (which is true for bugs.webkit.org)
+    _bugzilla_date_format = "%Y-%m-%d %H:%M:%S"
+
+    @classmethod
+    def _parse_date(cls, date_string):
+        (date, time, time_zone) = date_string.split(" ")
+        if time.count(':') == 1:
+            # Add seconds into the time.
+            time += ':0'
+        # Ignore the timezone because python doesn't understand timezones out of the box.
+        date_string = "%s %s" % (date, time)
+        return datetime.strptime(date_string, cls._bugzilla_date_format)
+
+    def _date_contents(self, soup):
+        return self._parse_date(self._string_contents(soup))
+
+    def _parse_attachment_element(self, element, bug_id):
+        attachment = {}
+        attachment['bug_id'] = bug_id
+        attachment['is_obsolete'] = (element.has_key('isobsolete') and element['isobsolete'] == "1")
+        attachment['is_patch'] = (element.has_key('ispatch') and element['ispatch'] == "1")
+        attachment['id'] = int(element.find('attachid').string)
+        # FIXME: No need to parse out the url here.
+        attachment['url'] = self.attachment_url_for_id(attachment['id'])
+        attachment["attach_date"] = self._date_contents(element.find("date"))
+        attachment['name'] = self._string_contents(element.find('desc'))
+        attachment['attacher_email'] = self._string_contents(element.find('attacher'))
+        attachment['type'] = self._string_contents(element.find('type'))
+        self._parse_attachment_flag(
+                element, 'review', attachment, 'reviewer_email')
+        self._parse_attachment_flag(
+                element, 'commit-queue', attachment, 'committer_email')
+        return attachment
+
+    def _parse_log_descr_element(self, element):
+        comment = {}
+        comment['comment_email'] = self._string_contents(element.find('who'))
+        comment['comment_date'] = self._date_contents(element.find('bug_when'))
+        comment['text'] = self._string_contents(element.find('thetext'))
+        return comment
+
+    def _parse_bugs_from_xml(self, page):
+        soup = BeautifulSoup(page)
+        # Without the unicode() call, BeautifulSoup occasionally complains of being
+        # passed None for no apparent reason.
+        return [Bug(self._parse_bug_dictionary_from_xml(unicode(bug_xml)), self) for bug_xml in soup('bug')]
+
+    def _parse_bug_dictionary_from_xml(self, page):
+        soup = BeautifulStoneSoup(page, convertEntities=BeautifulStoneSoup.XML_ENTITIES)
+        bug = {}
+        bug["id"] = int(soup.find("bug_id").string)
+        bug["title"] = self._string_contents(soup.find("short_desc"))
+        bug["bug_status"] = self._string_contents(soup.find("bug_status"))
+        dup_id = soup.find("dup_id")
+        if dup_id:
+            bug["dup_id"] = self._string_contents(dup_id)
+        bug["reporter_email"] = self._string_contents(soup.find("reporter"))
+        bug["assigned_to_email"] = self._string_contents(soup.find("assigned_to"))
+        bug["cc_emails"] = [self._string_contents(element) for element in soup.findAll('cc')]
+        bug["attachments"] = [self._parse_attachment_element(element, bug["id"]) for element in soup.findAll('attachment')]
+        bug["comments"] = [self._parse_log_descr_element(element) for element in soup.findAll('long_desc')]
+
+        return bug
+
+    # Makes testing fetch_*_from_bug() possible until we have a better
+    # BugzillaNetwork abstration.
+
+    def _fetch_bug_page(self, bug_id):
+        bug_url = self.bug_url_for_bug_id(bug_id, xml=True)
+        log("Fetching: %s" % bug_url)
+        return self.browser.open(bug_url)
+
+    def fetch_bug_dictionary(self, bug_id):
+        try:
+            return self._parse_bug_dictionary_from_xml(self._fetch_bug_page(bug_id))
+        except KeyboardInterrupt:
+            raise
+        except:
+            self.authenticate()
+            return self._parse_bug_dictionary_from_xml(self._fetch_bug_page(bug_id))
+
+    # FIXME: A BugzillaCache object should provide all these fetch_ methods.
+
+    def fetch_bug(self, bug_id):
+        return Bug(self.fetch_bug_dictionary(bug_id), self)
+
+    def fetch_attachment_contents(self, attachment_id):
+        attachment_url = self.attachment_url_for_id(attachment_id)
+        # We need to authenticate to download patches from security bugs.
+        self.authenticate()
+        return self.browser.open(attachment_url).read()
+
+    def _parse_bug_id_from_attachment_page(self, page):
+        # The "Up" relation happens to point to the bug.
+        up_link = BeautifulSoup(page).find('link', rel='Up')
+        if not up_link:
+            # This attachment does not exist (or you don't have permissions to
+            # view it).
+            return None
+        match = re.search("show_bug.cgi\?id=(?P<bug_id>\d+)", up_link['href'])
+        return int(match.group('bug_id'))
+
+    def bug_id_for_attachment_id(self, attachment_id):
+        self.authenticate()
+
+        attachment_url = self.attachment_url_for_id(attachment_id, 'edit')
+        log("Fetching: %s" % attachment_url)
+        page = self.browser.open(attachment_url)
+        return self._parse_bug_id_from_attachment_page(page)
+
+    # FIXME: This should just return Attachment(id), which should be able to
+    # lazily fetch needed data.
+
+    def fetch_attachment(self, attachment_id):
+        # We could grab all the attachment details off of the attachment edit
+        # page but we already have working code to do so off of the bugs page,
+        # so re-use that.
+        bug_id = self.bug_id_for_attachment_id(attachment_id)
+        if not bug_id:
+            return None
+        attachments = self.fetch_bug(bug_id).attachments(include_obsolete=True)
+        for attachment in attachments:
+            if attachment.id() == int(attachment_id):
+                return attachment
+        return None # This should never be hit.
+
+    def authenticate(self):
+        if self.authenticated:
+            return
+
+        credentials = Credentials(config_urls.bug_server_host, git_prefix="bugzilla")
+
+        attempts = 0
+        while not self.authenticated:
+            attempts += 1
+            username, password = credentials.read_credentials()
+
+            log("Logging in as %s..." % username)
+            self.browser.open(config_urls.bug_server_url +
+                              "index.cgi?GoAheadAndLogIn=1")
+            self.browser.select_form(name="login")
+            self.browser['Bugzilla_login'] = username
+            self.browser['Bugzilla_password'] = password
+            self.browser.find_control("Bugzilla_restrictlogin").items[0].selected = False
+            response = self.browser.submit()
+
+            match = re.search("<title>(.+?)</title>", response.read())
+            # If the resulting page has a title, and it contains the word
+            # "invalid" assume it's the login failure page.
+            if match and re.search("Invalid", match.group(1), re.IGNORECASE):
+                errorMessage = "Bugzilla login failed: %s" % match.group(1)
+                # raise an exception only if this was the last attempt
+                if attempts < 5:
+                    log(errorMessage)
+                else:
+                    raise Exception(errorMessage)
+            else:
+                self.authenticated = True
+                self.username = username
+
+    # FIXME: Use enum instead of two booleans
+    def _commit_queue_flag(self, mark_for_landing, mark_for_commit_queue):
+        if mark_for_landing:
+            user = self.committers.account_by_email(self.username)
+            mark_for_commit_queue = True
+            if not user:
+                log("Your Bugzilla login is not listed in committers.py. Uploading with cq? instead of cq+")
+                mark_for_landing = False
+            elif not user.can_commit:
+                log("You're not a committer yet or haven't updated committers.py yet. Uploading with cq? instead of cq+")
+                mark_for_landing = False
+
+        if mark_for_landing:
+            return '+'
+        if mark_for_commit_queue:
+            return '?'
+        return 'X'
+
+    # FIXME: mark_for_commit_queue and mark_for_landing should be joined into a single commit_flag argument.
+    def _fill_attachment_form(self,
+                              description,
+                              file_object,
+                              mark_for_review=False,
+                              mark_for_commit_queue=False,
+                              mark_for_landing=False,
+                              is_patch=False,
+                              filename=None,
+                              mimetype=None):
+        self.browser['description'] = description
+        if is_patch:
+            self.browser['ispatch'] = ("1",)
+        # FIXME: Should this use self._find_select_element_for_flag?
+        self.browser['flag_type-1'] = ('?',) if mark_for_review else ('X',)
+        self.browser['flag_type-3'] = (self._commit_queue_flag(mark_for_landing, mark_for_commit_queue),)
+
+        filename = filename or "%s.patch" % timestamp()
+        if not mimetype:
+            mimetypes.add_type('text/plain', '.patch')  # Make sure mimetypes knows about .patch
+            mimetype, _ = mimetypes.guess_type(filename)
+        if not mimetype:
+            mimetype = "text/plain"  # Bugzilla might auto-guess for us and we might not need this?
+        self.browser.add_file(file_object, mimetype, filename, 'data')
+
+    def _file_object_for_upload(self, file_or_string):
+        if hasattr(file_or_string, 'read'):
+            return file_or_string
+        # Only if file_or_string is not already encoded do we want to encode it.
+        if isinstance(file_or_string, unicode):
+            file_or_string = file_or_string.encode('utf-8')
+        return StringIO.StringIO(file_or_string)
+
+    # timestamp argument is just for unittests.
+    def _filename_for_upload(self, file_object, bug_id, extension="txt", timestamp=timestamp):
+        if hasattr(file_object, "name"):
+            return file_object.name
+        return "bug-%s-%s.%s" % (bug_id, timestamp(), extension)
+
+    def add_attachment_to_bug(self, bug_id, file_or_string, description, filename=None, comment_text=None, mimetype=None):
+        self.authenticate()
+        log('Adding attachment "%s" to %s' % (description, self.bug_url_for_bug_id(bug_id)))
+        self.browser.open(self.add_attachment_url(bug_id))
+        self.browser.select_form(name="entryform")
+        file_object = self._file_object_for_upload(file_or_string)
+        filename = filename or self._filename_for_upload(file_object, bug_id)
+        self._fill_attachment_form(description, file_object, filename=filename, mimetype=mimetype)
+        if comment_text:
+            log(comment_text)
+            self.browser['comment'] = comment_text
+        self.browser.submit()
+
+    # FIXME: The arguments to this function should be simplified and then
+    # this should be merged into add_attachment_to_bug
+    def add_patch_to_bug(self,
+                         bug_id,
+                         file_or_string,
+                         description,
+                         comment_text=None,
+                         mark_for_review=False,
+                         mark_for_commit_queue=False,
+                         mark_for_landing=False):
+        self.authenticate()
+        log('Adding patch "%s" to %s' % (description, self.bug_url_for_bug_id(bug_id)))
+
+        self.browser.open(self.add_attachment_url(bug_id))
+        self.browser.select_form(name="entryform")
+        file_object = self._file_object_for_upload(file_or_string)
+        filename = self._filename_for_upload(file_object, bug_id, extension="patch")
+        self._fill_attachment_form(description,
+                                   file_object,
+                                   mark_for_review=mark_for_review,
+                                   mark_for_commit_queue=mark_for_commit_queue,
+                                   mark_for_landing=mark_for_landing,
+                                   is_patch=True,
+                                   filename=filename)
+        if comment_text:
+            log(comment_text)
+            self.browser['comment'] = comment_text
+        self.browser.submit()
+
+    # FIXME: There has to be a more concise way to write this method.
+    def _check_create_bug_response(self, response_html):
+        match = re.search("<title>Bug (?P<bug_id>\d+) Submitted</title>",
+                          response_html)
+        if match:
+            return match.group('bug_id')
+
+        match = re.search(
+            '<div id="bugzilla-body">(?P<error_message>.+)<div id="footer">',
+            response_html,
+            re.DOTALL)
+        error_message = "FAIL"
+        if match:
+            text_lines = BeautifulSoup(
+                    match.group('error_message')).findAll(text=True)
+            error_message = "\n" + '\n'.join(
+                    ["  " + line.strip()
+                     for line in text_lines if line.strip()])
+        raise Exception("Bug not created: %s" % error_message)
+
+    def create_bug(self,
+                   bug_title,
+                   bug_description,
+                   component=None,
+                   diff=None,
+                   patch_description=None,
+                   cc=None,
+                   blocked=None,
+                   assignee=None,
+                   mark_for_review=False,
+                   mark_for_commit_queue=False):
+        self.authenticate()
+
+        log('Creating bug with title "%s"' % bug_title)
+        self.browser.open(config_urls.bug_server_url + "enter_bug.cgi?product=WebKit")
+        self.browser.select_form(name="Create")
+        component_items = self.browser.find_control('component').items
+        component_names = map(lambda item: item.name, component_items)
+        if not component:
+            component = "New Bugs"
+        if component not in component_names:
+            component = User.prompt_with_list("Please pick a component:", component_names)
+        self.browser["component"] = [component]
+        if cc:
+            self.browser["cc"] = cc
+        if blocked:
+            self.browser["blocked"] = unicode(blocked)
+        if not assignee:
+            assignee = self.username
+        if assignee and not self.browser.find_control("assigned_to").disabled:
+            self.browser["assigned_to"] = assignee
+        self.browser["short_desc"] = bug_title
+        self.browser["comment"] = bug_description
+
+        if diff:
+            # _fill_attachment_form expects a file-like object
+            # Patch files are already binary, so no encoding needed.
+            assert(isinstance(diff, str))
+            patch_file_object = StringIO.StringIO(diff)
+            self._fill_attachment_form(
+                    patch_description,
+                    patch_file_object,
+                    mark_for_review=mark_for_review,
+                    mark_for_commit_queue=mark_for_commit_queue,
+                    is_patch=True)
+
+        response = self.browser.submit()
+
+        bug_id = self._check_create_bug_response(response.read())
+        log("Bug %s created." % bug_id)
+        log("%sshow_bug.cgi?id=%s" % (config_urls.bug_server_url, bug_id))
+        return bug_id
+
+    def _find_select_element_for_flag(self, flag_name):
+        # FIXME: This will break if we ever re-order attachment flags
+        if flag_name == "review":
+            return self.browser.find_control(type='select', nr=0)
+        elif flag_name == "commit-queue":
+            return self.browser.find_control(type='select', nr=1)
+        raise Exception("Don't know how to find flag named \"%s\"" % flag_name)
+
+    def clear_attachment_flags(self,
+                               attachment_id,
+                               additional_comment_text=None):
+        self.authenticate()
+
+        comment_text = "Clearing flags on attachment: %s" % attachment_id
+        if additional_comment_text:
+            comment_text += "\n\n%s" % additional_comment_text
+        log(comment_text)
+
+        self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
+        self.browser.select_form(nr=1)
+        self.browser.set_value(comment_text, name='comment', nr=0)
+        self._find_select_element_for_flag('review').value = ("X",)
+        self._find_select_element_for_flag('commit-queue').value = ("X",)
+        self.browser.submit()
+
+    def set_flag_on_attachment(self,
+                               attachment_id,
+                               flag_name,
+                               flag_value,
+                               comment_text=None,
+                               additional_comment_text=None):
+        # FIXME: We need a way to test this function on a live bugzilla
+        # instance.
+
+        self.authenticate()
+
+        # FIXME: additional_comment_text seems useless and should be merged into comment-text.
+        if additional_comment_text:
+            comment_text += "\n\n%s" % additional_comment_text
+        log(comment_text)
+
+        self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
+        self.browser.select_form(nr=1)
+
+        if comment_text:
+            self.browser.set_value(comment_text, name='comment', nr=0)
+
+        self._find_select_element_for_flag(flag_name).value = (flag_value,)
+        self.browser.submit()
+
+    # FIXME: All of these bug editing methods have a ridiculous amount of
+    # copy/paste code.
+
+    def obsolete_attachment(self, attachment_id, comment_text=None):
+        self.authenticate()
+
+        log("Obsoleting attachment: %s" % attachment_id)
+        self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
+        self.browser.select_form(nr=1)
+        self.browser.find_control('isobsolete').items[0].selected = True
+        # Also clear any review flag (to remove it from review/commit queues)
+        self._find_select_element_for_flag('review').value = ("X",)
+        self._find_select_element_for_flag('commit-queue').value = ("X",)
+        if comment_text:
+            log(comment_text)
+            # Bugzilla has two textareas named 'comment', one is somehow
+            # hidden.  We want the first.
+            self.browser.set_value(comment_text, name='comment', nr=0)
+        self.browser.submit()
+
+    def add_cc_to_bug(self, bug_id, email_address_list):
+        self.authenticate()
+
+        log("Adding %s to the CC list for bug %s" % (email_address_list, bug_id))
+        self.browser.open(self.bug_url_for_bug_id(bug_id))
+        self.browser.select_form(name="changeform")
+        self.browser["newcc"] = ", ".join(email_address_list)
+        self.browser.submit()
+
+    def post_comment_to_bug(self, bug_id, comment_text, cc=None):
+        self.authenticate()
+
+        log("Adding comment to bug %s" % bug_id)
+        self.browser.open(self.bug_url_for_bug_id(bug_id))
+        self.browser.select_form(name="changeform")
+        self.browser["comment"] = comment_text
+        if cc:
+            self.browser["newcc"] = ", ".join(cc)
+        self.browser.submit()
+
+    def close_bug_as_fixed(self, bug_id, comment_text=None):
+        self.authenticate()
+
+        log("Closing bug %s as fixed" % bug_id)
+        self.browser.open(self.bug_url_for_bug_id(bug_id))
+        self.browser.select_form(name="changeform")
+        if comment_text:
+            self.browser['comment'] = comment_text
+        self.browser['bug_status'] = ['RESOLVED']
+        self.browser['resolution'] = ['FIXED']
+        self.browser.submit()
+
+    def _has_control(self, form, id):
+        return id in [control.id for control in form.controls]
+
+    def reassign_bug(self, bug_id, assignee=None, comment_text=None):
+        self.authenticate()
+
+        if not assignee:
+            assignee = self.username
+
+        log("Assigning bug %s to %s" % (bug_id, assignee))
+        self.browser.open(self.bug_url_for_bug_id(bug_id))
+        self.browser.select_form(name="changeform")
+
+        if not self._has_control(self.browser, "assigned_to"):
+            log("""Failed to assign bug to you (can't find assigned_to) control.
+Do you have EditBugs privileges at bugs.webkit.org?
+https://bugs.webkit.org/userprefs.cgi?tab=permissions
+
+If not, you should email webkit-committers@lists.webkit.org or ask in #webkit
+for someone to add EditBugs to your bugs.webkit.org account.""")
+            return
+
+        if comment_text:
+            log(comment_text)
+            self.browser["comment"] = comment_text
+        self.browser["assigned_to"] = assignee
+        self.browser.submit()
+
+    def reopen_bug(self, bug_id, comment_text):
+        self.authenticate()
+
+        log("Re-opening bug %s" % bug_id)
+        # Bugzilla requires a comment when re-opening a bug, so we know it will
+        # never be None.
+        log(comment_text)
+        self.browser.open(self.bug_url_for_bug_id(bug_id))
+        self.browser.select_form(name="changeform")
+        bug_status = self.browser.find_control("bug_status", type="select")
+        # This is a hack around the fact that ClientForm.ListControl seems to
+        # have no simpler way to ask if a control has an item named "REOPENED"
+        # without using exceptions for control flow.
+        possible_bug_statuses = map(lambda item: item.name, bug_status.items)
+        if "REOPENED" in possible_bug_statuses:
+            bug_status.value = ["REOPENED"]
+        # If the bug was never confirmed it will not have a "REOPENED"
+        # state, but only an "UNCONFIRMED" state.
+        elif "UNCONFIRMED" in possible_bug_statuses:
+            bug_status.value = ["UNCONFIRMED"]
+        else:
+            # FIXME: This logic is slightly backwards.  We won't print this
+            # message if the bug is already open with state "UNCONFIRMED".
+            log("Did not reopen bug %s, it appears to already be open with status %s." % (bug_id, bug_status.value))
+        self.browser['comment'] = comment_text
+        self.browser.submit()
diff --git a/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_mock.py b/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_mock.py
new file mode 100644
index 0000000..71b080c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_mock.py
@@ -0,0 +1,432 @@
+# Copyright (C) 2011 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 datetime
+
+from .bug import Bug
+from .attachment import Attachment
+from webkitpy.common.config.committers import CommitterList, Reviewer
+
+from webkitpy.common.system.deprecated_logging import log
+
+
+def _id_to_object_dictionary(*objects):
+    dictionary = {}
+    for thing in objects:
+        dictionary[thing["id"]] = thing
+    return dictionary
+
+# Testing
+
+
+_patch1 = {
+    "id": 10000,
+    "bug_id": 50000,
+    "url": "http://example.com/10000",
+    "name": "Patch1",
+    "is_obsolete": False,
+    "is_patch": True,
+    "review": "+",
+    "reviewer_email": "foo@bar.com",
+    "commit-queue": "+",
+    "committer_email": "foo@bar.com",
+    "attacher_email": "Contributer1",
+}
+
+
+_patch2 = {
+    "id": 10001,
+    "bug_id": 50000,
+    "url": "http://example.com/10001",
+    "name": "Patch2",
+    "is_obsolete": False,
+    "is_patch": True,
+    "review": "+",
+    "reviewer_email": "reviewer2@webkit.org",
+    "commit-queue": "+",
+    "committer_email": "non-committer@example.com",
+    "attacher_email": "eric@webkit.org",
+}
+
+
+_patch3 = {
+    "id": 10002,
+    "bug_id": 50001,
+    "url": "http://example.com/10002",
+    "name": "Patch3",
+    "is_obsolete": False,
+    "is_patch": True,
+    "review": "?",
+    "commit-queue": "-",
+    "attacher_email": "eric@webkit.org",
+    "attach_date": datetime.datetime.today(),
+}
+
+
+_patch4 = {
+    "id": 10003,
+    "bug_id": 50003,
+    "url": "http://example.com/10002",
+    "name": "Patch3",
+    "is_obsolete": False,
+    "is_patch": True,
+    "review": "+",
+    "commit-queue": "?",
+    "reviewer_email": "foo@bar.com",
+    "attacher_email": "Contributer2",
+}
+
+
+_patch5 = {
+    "id": 10004,
+    "bug_id": 50003,
+    "url": "http://example.com/10002",
+    "name": "Patch5",
+    "is_obsolete": False,
+    "is_patch": True,
+    "review": "+",
+    "reviewer_email": "foo@bar.com",
+    "attacher_email": "eric@webkit.org",
+}
+
+
+_patch6 = {  # Valid committer, but no reviewer.
+    "id": 10005,
+    "bug_id": 50003,
+    "url": "http://example.com/10002",
+    "name": "ROLLOUT of r3489",
+    "is_obsolete": False,
+    "is_patch": True,
+    "commit-queue": "+",
+    "committer_email": "foo@bar.com",
+    "attacher_email": "eric@webkit.org",
+}
+
+
+_patch7 = {  # Valid review, patch is marked obsolete.
+    "id": 10006,
+    "bug_id": 50002,
+    "url": "http://example.com/10002",
+    "name": "Patch7",
+    "is_obsolete": True,
+    "is_patch": True,
+    "review": "+",
+    "reviewer_email": "foo@bar.com",
+    "attacher_email": "eric@webkit.org",
+}
+
+
+# This matches one of Bug.unassigned_emails
+_unassigned_email = "webkit-unassigned@lists.webkit.org"
+# This is needed for the FlakyTestReporter to believe the bug
+# was filed by one of the webkitpy bots.
+_commit_queue_email = "commit-queue@webkit.org"
+
+
+_bug1 = {
+    "id": 50000,
+    "title": "Bug with two r+'d and cq+'d patches, one of which has an "
+             "invalid commit-queue setter.",
+    "reporter_email": "foo@foo.com",
+    "assigned_to_email": _unassigned_email,
+    "cc_emails": [],
+    "attachments": [_patch1, _patch2],
+    "bug_status": "UNCONFIRMED",
+    "comments": [],
+}
+
+
+_bug2 = {
+    "id": 50001,
+    "title": "Bug with a patch needing review.",
+    "reporter_email": "eric@webkit.org",
+    "assigned_to_email": "foo@foo.com",
+    "cc_emails": ["abarth@webkit.org", ],
+    "attachments": [_patch3],
+    "bug_status": "ASSIGNED",
+    "comments": [{"comment_date":  datetime.datetime(2011, 6, 11, 9, 4, 3),
+                  "comment_email": "bar@foo.com",
+                  "text": "Message1.\nCommitted r35: <http://trac.webkit.org/changeset/35>",
+                  },
+                 ],
+}
+
+
+_bug3 = {
+    "id": 50002,
+    "title": "The third bug",
+    "reporter_email": "foo@foo.com",
+    "assigned_to_email": _unassigned_email,
+    "cc_emails": [],
+    "attachments": [_patch7],
+    "bug_status": "NEW",
+    "comments":  [{"comment_date":  datetime.datetime(2011, 6, 11, 9, 4, 3),
+                   "comment_email": "bar@foo.com",
+                   "text": "Committed r30: <http://trac.webkit.org/changeset/30>",
+                   },
+                  {"comment_date":  datetime.datetime(2011, 6, 11, 9, 4, 3),
+                   "comment_email": "bar@foo.com",
+                   "text": "Committed r31: <http://trac.webkit.org/changeset/31>",
+                   },
+                  ],
+}
+
+
+_bug4 = {
+    "id": 50003,
+    "title": "The fourth bug",
+    "reporter_email": "foo@foo.com",
+    "assigned_to_email": "foo@foo.com",
+    "cc_emails": [],
+    "attachments": [_patch4, _patch5, _patch6],
+    "bug_status": "REOPENED",
+    "comments": [{"comment_date":  datetime.datetime(2011, 6, 11, 9, 4, 3),
+                  "comment_email": "bar@foo.com",
+                  "text": "Committed r25: <http://trac.webkit.org/changeset/30>",
+                  },
+                 {"comment_date":  datetime.datetime(2011, 6, 11, 9, 4, 3),
+                  "comment_email": "bar@foo.com",
+                  "text": "Rolled out in <http://trac.webkit.org/changeset/26",
+                  },
+                 ],
+}
+
+
+_bug5 = {
+    "id": 50004,
+    "title": "The fifth bug",
+    "reporter_email": _commit_queue_email,
+    "assigned_to_email": "foo@foo.com",
+    "cc_emails": [],
+    "attachments": [],
+    "bug_status": "RESOLVED",
+    "dup_id": 50002,
+    "comments": [{"comment_date":  datetime.datetime(2011, 6, 11, 9, 4, 3),
+                  "comment_email": "bar@foo.com",
+                  "text": "Committed r15: <http://trac.webkit.org/changeset/15>",
+                  },
+                 ],
+
+}
+
+
+class MockBugzillaQueries(object):
+
+    def __init__(self, bugzilla):
+        self._bugzilla = bugzilla
+
+    def _all_bugs(self):
+        return map(lambda bug_dictionary: Bug(bug_dictionary, self._bugzilla),
+                   self._bugzilla.bug_cache.values())
+
+    def fetch_bug_ids_from_commit_queue(self):
+        bugs_with_commit_queued_patches = filter(
+                lambda bug: bug.commit_queued_patches(),
+                self._all_bugs())
+        return map(lambda bug: bug.id(), bugs_with_commit_queued_patches)
+
+    def fetch_attachment_ids_from_review_queue(self):
+        unreviewed_patches = sum([bug.unreviewed_patches()
+                                  for bug in self._all_bugs()], [])
+        return map(lambda patch: patch.id(), unreviewed_patches)
+
+    def fetch_patches_from_commit_queue(self):
+        return sum([bug.commit_queued_patches()
+                    for bug in self._all_bugs()], [])
+
+    def fetch_bug_ids_from_pending_commit_list(self):
+        bugs_with_reviewed_patches = filter(lambda bug: bug.reviewed_patches(),
+                                            self._all_bugs())
+        bug_ids = map(lambda bug: bug.id(), bugs_with_reviewed_patches)
+        # NOTE: This manual hack here is to allow testing logging in
+        # test_assign_to_committer the real pending-commit query on bugzilla
+        # will return bugs with patches which have r+, but are also obsolete.
+        return bug_ids + [50002]
+
+    def fetch_bugs_from_review_queue(self, cc_email=None):
+        unreviewed_bugs = [bug for bug in self._all_bugs() if bug.unreviewed_patches()]
+
+        if cc_email:
+            return [bug for bug in unreviewed_bugs if cc_email in bug.cc_emails()]
+
+        return unreviewed_bugs
+
+    def fetch_patches_from_pending_commit_list(self):
+        return sum([bug.reviewed_patches() for bug in self._all_bugs()], [])
+
+    def fetch_bugs_matching_search(self, search_string):
+        return [self._bugzilla.fetch_bug(50004), self._bugzilla.fetch_bug(50003)]
+
+    def fetch_bugs_matching_quicksearch(self, search_string):
+        return [self._bugzilla.fetch_bug(50001), self._bugzilla.fetch_bug(50002),
+                self._bugzilla.fetch_bug(50003), self._bugzilla.fetch_bug(50004)]
+
+
+_mock_reviewers = [Reviewer("Foo Bar", "foo@bar.com"),
+                   Reviewer("Reviewer2", "reviewer2@webkit.org")]
+
+
+# FIXME: Bugzilla is the wrong Mock-point.  Once we have a BugzillaNetwork
+#        class we should mock that instead.
+# Most of this class is just copy/paste from Bugzilla.
+class MockBugzilla(object):
+
+    bug_server_url = "http://example.com"
+
+    bug_cache = _id_to_object_dictionary(_bug1, _bug2, _bug3, _bug4, _bug5)
+
+    attachment_cache = _id_to_object_dictionary(_patch1,
+                                                _patch2,
+                                                _patch3,
+                                                _patch4,
+                                                _patch5,
+                                                _patch6,
+                                                _patch7)
+
+    def __init__(self):
+        self.queries = MockBugzillaQueries(self)
+        # FIXME: This should move onto the Host object, and we should use a MockCommitterList
+        self.committers = CommitterList(reviewers=_mock_reviewers)
+        self.username = None
+        self._override_patch = None
+
+    def authenticate(self):
+        self.username = "username@webkit.org"
+
+    def create_bug(self,
+                   bug_title,
+                   bug_description,
+                   component=None,
+                   diff=None,
+                   patch_description=None,
+                   cc=None,
+                   blocked=None,
+                   mark_for_review=False,
+                   mark_for_commit_queue=False):
+        log("MOCK create_bug")
+        log("bug_title: %s" % bug_title)
+        log("bug_description: %s" % bug_description)
+        if component:
+            log("component: %s" % component)
+        if cc:
+            log("cc: %s" % cc)
+        if blocked:
+            log("blocked: %s" % blocked)
+        return 60001
+
+    def quips(self):
+        return ["Good artists copy. Great artists steal. - Pablo Picasso"]
+
+    def fetch_bug(self, bug_id):
+        return Bug(self.bug_cache.get(int(bug_id)), self)
+
+    def set_override_patch(self, patch):
+        self._override_patch = patch
+
+    def fetch_attachment(self, attachment_id):
+        if self._override_patch:
+            return self._override_patch
+
+        attachment_dictionary = self.attachment_cache.get(attachment_id)
+        if not attachment_dictionary:
+            print "MOCK: fetch_attachment: %s is not a known attachment id" % attachment_id
+            return None
+        bug = self.fetch_bug(attachment_dictionary["bug_id"])
+        for attachment in bug.attachments(include_obsolete=True):
+            if attachment.id() == int(attachment_id):
+                return attachment
+
+    def bug_url_for_bug_id(self, bug_id):
+        return "%s/%s" % (self.bug_server_url, bug_id)
+
+    def fetch_bug_dictionary(self, bug_id):
+        return self.bug_cache.get(bug_id)
+
+    def attachment_url_for_id(self, attachment_id, action="view"):
+        action_param = ""
+        if action and action != "view":
+            action_param = "&action=%s" % action
+        return "%s/%s%s" % (self.bug_server_url, attachment_id, action_param)
+
+    def reassign_bug(self, bug_id, assignee=None, comment_text=None):
+        log("MOCK reassign_bug: bug_id=%s, assignee=%s" % (bug_id, assignee))
+        if comment_text:
+            log("-- Begin comment --")
+            log(comment_text)
+            log("-- End comment --")
+
+    def set_flag_on_attachment(self,
+                               attachment_id,
+                               flag_name,
+                               flag_value,
+                               comment_text=None,
+                               additional_comment_text=None):
+        log("MOCK setting flag '%s' to '%s' on attachment '%s' with comment '%s' and additional comment '%s'" % (
+            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 ---\n%s\n--- End comment ---\n" % (
+            bug_id, cc, comment_text))
+
+    def add_attachment_to_bug(self, bug_id, file_or_string, description, filename=None, comment_text=None, mimetype=None):
+        log("MOCK add_attachment_to_bug: bug_id=%s, description=%s filename=%s mimetype=%s" %
+            (bug_id, description, filename, mimetype))
+        if comment_text:
+            log("-- Begin comment --")
+            log(comment_text)
+            log("-- End comment --")
+
+    def add_patch_to_bug(self,
+                         bug_id,
+                         diff,
+                         description,
+                         comment_text=None,
+                         mark_for_review=False,
+                         mark_for_commit_queue=False,
+                         mark_for_landing=False):
+        log("MOCK add_patch_to_bug: bug_id=%s, description=%s, mark_for_review=%s, mark_for_commit_queue=%s, mark_for_landing=%s" %
+            (bug_id, description, mark_for_review, mark_for_commit_queue, mark_for_landing))
+        if comment_text:
+            log("-- Begin comment --")
+            log(comment_text)
+            log("-- End comment --")
+
+    def add_cc_to_bug(self, bug_id, ccs):
+        pass
+
+    def obsolete_attachment(self, attachment_id, message=None):
+        pass
+
+    def reopen_bug(self, bug_id, message):
+        log("MOCK reopen_bug %s with comment '%s'" % (bug_id, message))
+
+    def close_bug_as_fixed(self, bug_id, message):
+        pass
+
+    def clear_attachment_flags(self, attachment_id, message):
+        pass
diff --git a/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py b/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py
new file mode 100644
index 0000000..6108b5e
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py
@@ -0,0 +1,550 @@
+# Copyright (C) 2011 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 datetime
+import StringIO
+
+from .bugzilla import Bugzilla, BugzillaQueries, EditUsersParser
+
+from webkitpy.common.config import urls
+from webkitpy.common.config.committers import Reviewer, Committer, Contributor, CommitterList
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.net.web_mock import MockBrowser
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup
+
+
+class BugzillaTest(unittest.TestCase):
+    _example_attachment = '''
+        <attachment
+          isobsolete="1"
+          ispatch="1"
+          isprivate="0"
+        >
+        <attachid>33721</attachid>
+        <date>2009-07-29 10:23 PDT</date>
+        <desc>Fixed whitespace issue</desc>
+        <filename>patch</filename>
+        <type>text/plain</type>
+        <size>9719</size>
+        <attacher>christian.plesner.hansen@gmail.com</attacher>
+          <flag name="review"
+                id="17931"
+                status="+"
+                setter="one@test.com"
+           />
+          <flag name="commit-queue"
+                id="17932"
+                status="+"
+                setter="two@test.com"
+           />
+        </attachment>
+'''
+    _expected_example_attachment_parsing = {
+        'attach_date': datetime.datetime(2009, 07, 29, 10, 23),
+        'bug_id' : 100,
+        'is_obsolete' : True,
+        'is_patch' : True,
+        'id' : 33721,
+        'url' : "https://bugs.webkit.org/attachment.cgi?id=33721",
+        'name' : "Fixed whitespace issue",
+        'type' : "text/plain",
+        'review' : '+',
+        'reviewer_email' : 'one@test.com',
+        'commit-queue' : '+',
+        'committer_email' : 'two@test.com',
+        'attacher_email' : 'christian.plesner.hansen@gmail.com',
+    }
+
+    def test_url_creation(self):
+        # FIXME: These would be all better as doctests
+        bugs = Bugzilla()
+        self.assertEquals(None, bugs.bug_url_for_bug_id(None))
+        self.assertEquals(None, bugs.short_bug_url_for_bug_id(None))
+        self.assertEquals(None, bugs.attachment_url_for_id(None))
+
+    def test_parse_bug_id(self):
+        # Test that we can parse the urls we produce.
+        bugs = Bugzilla()
+        self.assertEquals(12345, urls.parse_bug_id(bugs.short_bug_url_for_bug_id(12345)))
+        self.assertEquals(12345, urls.parse_bug_id(bugs.bug_url_for_bug_id(12345)))
+        self.assertEquals(12345, urls.parse_bug_id(bugs.bug_url_for_bug_id(12345, xml=True)))
+
+    _bug_xml = """
+    <bug>
+          <bug_id>32585</bug_id>
+          <creation_ts>2009-12-15 15:17 PST</creation_ts>
+          <short_desc>bug to test webkit-patch&apos;s and commit-queue&apos;s failures</short_desc>
+          <delta_ts>2009-12-27 21:04:50 PST</delta_ts>
+          <reporter_accessible>1</reporter_accessible>
+          <cclist_accessible>1</cclist_accessible>
+          <classification_id>1</classification_id>
+          <classification>Unclassified</classification>
+          <product>WebKit</product>
+          <component>Tools / Tests</component>
+          <version>528+ (Nightly build)</version>
+          <rep_platform>PC</rep_platform>
+          <op_sys>Mac OS X 10.5</op_sys>
+          <bug_status>NEW</bug_status>
+          <priority>P2</priority>
+          <bug_severity>Normal</bug_severity>
+          <target_milestone>---</target_milestone>
+          <everconfirmed>1</everconfirmed>
+          <reporter name="Eric Seidel">eric@webkit.org</reporter>
+          <assigned_to name="Nobody">webkit-unassigned@lists.webkit.org</assigned_to>
+          <cc>foo@bar.com</cc>
+    <cc>example@example.com</cc>
+          <long_desc isprivate="0">
+            <who name="Eric Seidel">eric@webkit.org</who>
+            <bug_when>2009-12-15 15:17:28 PST</bug_when>
+            <thetext>bug to test webkit-patch and commit-queue failures
+
+Ignore this bug.  Just for testing failure modes of webkit-patch and the commit-queue.</thetext>
+          </long_desc>
+          <attachment
+              isobsolete="0"
+              ispatch="1"
+              isprivate="0"
+          >
+            <attachid>45548</attachid>
+            <date>2009-12-27 23:51 PST</date>
+            <desc>Patch</desc>
+            <filename>bug-32585-20091228005112.patch</filename>
+            <type>text/plain</type>
+            <size>10882</size>
+            <attacher>mjs@apple.com</attacher>
+
+              <token>1261988248-dc51409e9c421a4358f365fa8bec8357</token>
+              <data encoding="base64">SW5kZXg6IFdlYktpdC9tYWMvQ2hhbmdlTG9nCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09
+removed-because-it-was-really-long
+ZEZpbmlzaExvYWRXaXRoUmVhc29uOnJlYXNvbl07Cit9CisKIEBlbmQKIAogI2VuZGlmCg==
+</data>
+
+            <flag name="review"
+                id="27602"
+                status="?"
+                setter="mjs@apple.com"
+            />
+        </attachment>
+    </bug>
+"""
+
+    _single_bug_xml = """
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<!DOCTYPE bugzilla SYSTEM "https://bugs.webkit.org/bugzilla.dtd">
+<bugzilla version="3.2.3"
+          urlbase="https://bugs.webkit.org/"
+          maintainer="admin@webkit.org"
+          exporter="eric@webkit.org"
+>
+%s
+</bugzilla>
+""" % _bug_xml
+
+    _expected_example_bug_parsing = {
+        "id" : 32585,
+        "title" : u"bug to test webkit-patch's and commit-queue's failures",
+        "cc_emails" : ["foo@bar.com", "example@example.com"],
+        "reporter_email" : "eric@webkit.org",
+        "assigned_to_email" : "webkit-unassigned@lists.webkit.org",
+        "bug_status": "NEW",
+        "attachments" : [{
+            "attach_date": datetime.datetime(2009, 12, 27, 23, 51),
+            'name': u'Patch',
+            'url' : "https://bugs.webkit.org/attachment.cgi?id=45548",
+            'is_obsolete': False,
+            'review': '?',
+            'is_patch': True,
+            'attacher_email': 'mjs@apple.com',
+            'bug_id': 32585,
+            'type': 'text/plain',
+            'id': 45548
+        }],
+        "comments" : [{
+                'comment_date': datetime.datetime(2009, 12, 15, 15, 17, 28),
+                'comment_email': 'eric@webkit.org',
+                'text': """bug to test webkit-patch and commit-queue failures
+
+Ignore this bug.  Just for testing failure modes of webkit-patch and the commit-queue.""",
+        }]
+    }
+
+    # FIXME: This should move to a central location and be shared by more unit tests.
+    def _assert_dictionaries_equal(self, actual, expected):
+        # Make sure we aren't parsing more or less than we expect
+        self.assertEquals(sorted(actual.keys()), sorted(expected.keys()))
+
+        for key, expected_value in expected.items():
+            self.assertEquals(actual[key], expected_value, ("Failure for key: %s: Actual='%s' Expected='%s'" % (key, actual[key], expected_value)))
+
+    def test_parse_bug_dictionary_from_xml(self):
+        bug = Bugzilla()._parse_bug_dictionary_from_xml(self._single_bug_xml)
+        self._assert_dictionaries_equal(bug, self._expected_example_bug_parsing)
+
+    _sample_multi_bug_xml = """
+<bugzilla version="3.2.3" urlbase="https://bugs.webkit.org/" maintainer="admin@webkit.org" exporter="eric@webkit.org">
+    %s
+    %s
+</bugzilla>
+""" % (_bug_xml, _bug_xml)
+
+    def test_parse_bugs_from_xml(self):
+        bugzilla = Bugzilla()
+        bugs = bugzilla._parse_bugs_from_xml(self._sample_multi_bug_xml)
+        self.assertEquals(len(bugs), 2)
+        self.assertEquals(bugs[0].id(), self._expected_example_bug_parsing['id'])
+        bugs = bugzilla._parse_bugs_from_xml("")
+        self.assertEquals(len(bugs), 0)
+
+    # This could be combined into test_bug_parsing later if desired.
+    def test_attachment_parsing(self):
+        bugzilla = Bugzilla()
+        soup = BeautifulSoup(self._example_attachment)
+        attachment_element = soup.find("attachment")
+        attachment = bugzilla._parse_attachment_element(attachment_element, self._expected_example_attachment_parsing['bug_id'])
+        self.assertTrue(attachment)
+        self._assert_dictionaries_equal(attachment, self._expected_example_attachment_parsing)
+
+    _sample_attachment_detail_page = """
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+                      "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+  <head>
+    <title>
+  Attachment 41073 Details for Bug 27314</title>
+<link rel="Top" href="https://bugs.webkit.org/">
+    <link rel="Up" href="show_bug.cgi?id=27314">
+"""
+
+    def test_attachment_detail_bug_parsing(self):
+        bugzilla = Bugzilla()
+        self.assertEquals(27314, bugzilla._parse_bug_id_from_attachment_page(self._sample_attachment_detail_page))
+
+    def test_add_cc_to_bug(self):
+        bugzilla = Bugzilla()
+        bugzilla.browser = MockBrowser()
+        bugzilla.authenticate = lambda: None
+        expected_stderr = "Adding ['adam@example.com'] to the CC list for bug 42\n"
+        OutputCapture().assert_outputs(self, bugzilla.add_cc_to_bug, [42, ["adam@example.com"]], expected_stderr=expected_stderr)
+
+    def _mock_control_item(self, name):
+        mock_item = Mock()
+        mock_item.name = name
+        return mock_item
+
+    def _mock_find_control(self, item_names=[], selected_index=0):
+        mock_control = Mock()
+        mock_control.items = [self._mock_control_item(name) for name in item_names]
+        mock_control.value = [item_names[selected_index]] if item_names else None
+        return lambda name, type: mock_control
+
+    def _assert_reopen(self, item_names=None, selected_index=None, extra_stderr=None):
+        bugzilla = Bugzilla()
+        bugzilla.browser = MockBrowser()
+        bugzilla.authenticate = lambda: None
+
+        mock_find_control = self._mock_find_control(item_names, selected_index)
+        bugzilla.browser.find_control = mock_find_control
+        expected_stderr = "Re-opening bug 42\n['comment']\n"
+        if extra_stderr:
+            expected_stderr += extra_stderr
+        OutputCapture().assert_outputs(self, bugzilla.reopen_bug, [42, ["comment"]], expected_stderr=expected_stderr)
+
+    def test_reopen_bug(self):
+        self._assert_reopen(item_names=["REOPENED", "RESOLVED", "CLOSED"], selected_index=1)
+        self._assert_reopen(item_names=["UNCONFIRMED", "RESOLVED", "CLOSED"], selected_index=1)
+        extra_stderr = "Did not reopen bug 42, it appears to already be open with status ['NEW'].\n"
+        self._assert_reopen(item_names=["NEW", "RESOLVED"], selected_index=0, extra_stderr=extra_stderr)
+
+    def test_file_object_for_upload(self):
+        bugzilla = Bugzilla()
+        file_object = StringIO.StringIO()
+        unicode_tor = u"WebKit \u2661 Tor Arne Vestb\u00F8!"
+        utf8_tor = unicode_tor.encode("utf-8")
+        self.assertEqual(bugzilla._file_object_for_upload(file_object), file_object)
+        self.assertEqual(bugzilla._file_object_for_upload(utf8_tor).read(), utf8_tor)
+        self.assertEqual(bugzilla._file_object_for_upload(unicode_tor).read(), utf8_tor)
+
+    def test_filename_for_upload(self):
+        bugzilla = Bugzilla()
+        mock_file = Mock()
+        mock_file.name = "foo"
+        self.assertEqual(bugzilla._filename_for_upload(mock_file, 1234), 'foo')
+        mock_timestamp = lambda: "now"
+        filename = bugzilla._filename_for_upload(StringIO.StringIO(), 1234, extension="patch", timestamp=mock_timestamp)
+        self.assertEqual(filename, "bug-1234-now.patch")
+
+    def test_commit_queue_flag(self):
+        bugzilla = Bugzilla()
+
+        bugzilla.committers = CommitterList(reviewers=[Reviewer("WebKit Reviewer", "reviewer@webkit.org")],
+            committers=[Committer("WebKit Committer", "committer@webkit.org")],
+            contributors=[Contributor("WebKit Contributor", "contributor@webkit.org")],
+            watchers=[])
+
+        def assert_commit_queue_flag(mark_for_landing, mark_for_commit_queue, expected, username=None):
+            bugzilla.username = username
+            capture = OutputCapture()
+            capture.capture_output()
+            try:
+                self.assertEqual(bugzilla._commit_queue_flag(mark_for_landing=mark_for_landing, mark_for_commit_queue=mark_for_commit_queue), expected)
+            finally:
+                capture.restore_output()
+
+        assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=False, expected='X', username='unknown@webkit.org')
+        assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='unknown@webkit.org')
+        assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='unknown@webkit.org')
+        assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=True, expected='?', username='unknown@webkit.org')
+
+        assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=False, expected='X', username='contributor@webkit.org')
+        assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='contributor@webkit.org')
+        assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=False, expected='?', username='contributor@webkit.org')
+        assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=True, expected='?', username='contributor@webkit.org')
+
+        assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=False, expected='X', username='committer@webkit.org')
+        assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='committer@webkit.org')
+        assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=False, expected='+', username='committer@webkit.org')
+        assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=True, expected='+', username='committer@webkit.org')
+
+        assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=False, expected='X', username='reviewer@webkit.org')
+        assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='reviewer@webkit.org')
+        assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=False, expected='+', username='reviewer@webkit.org')
+        assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=True, expected='+', username='reviewer@webkit.org')
+
+
+class BugzillaQueriesTest(unittest.TestCase):
+    _sample_request_page = """
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+                      "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+  <head>
+    <title>Request Queue</title>
+  </head>
+<body>
+
+<h3>Flag: review</h3>
+  <table class="requests" cellspacing="0" cellpadding="4" border="1">
+    <tr>
+        <th>Requester</th>
+        <th>Requestee</th>
+        <th>Bug</th>
+        <th>Attachment</th>
+        <th>Created</th>
+    </tr>
+    <tr>
+        <td>Shinichiro Hamaji &lt;hamaji&#64;chromium.org&gt;</td>
+        <td></td>
+        <td><a href="show_bug.cgi?id=30015">30015: text-transform:capitalize is failing in CSS2.1 test suite</a></td>
+        <td><a href="attachment.cgi?id=40511&amp;action=review">
+40511: Patch v0</a></td>
+        <td>2009-10-02 04:58 PST</td>
+    </tr>
+    <tr>
+        <td>Zan Dobersek &lt;zandobersek&#64;gmail.com&gt;</td>
+        <td></td>
+        <td><a href="show_bug.cgi?id=26304">26304: [GTK] Add controls for playing html5 video.</a></td>
+        <td><a href="attachment.cgi?id=40722&amp;action=review">
+40722: Media controls, the simple approach</a></td>
+        <td>2009-10-06 09:13 PST</td>
+    </tr>
+    <tr>
+        <td>Zan Dobersek &lt;zandobersek&#64;gmail.com&gt;</td>
+        <td></td>
+        <td><a href="show_bug.cgi?id=26304">26304: [GTK] Add controls for playing html5 video.</a></td>
+        <td><a href="attachment.cgi?id=40723&amp;action=review">
+40723: Adjust the media slider thumb size</a></td>
+        <td>2009-10-06 09:15 PST</td>
+    </tr>
+  </table>
+</body>
+</html>
+"""
+    _sample_quip_page = u"""
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+                      "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+  <head>
+    <title>Bugzilla Quip System</title>
+  </head>
+  <body>
+    <h2>
+
+      Existing quips:
+    </h2>
+    <ul>
+        <li>Everything should be made as simple as possible, but not simpler. - Albert Einstein</li>
+        <li>Good artists copy. Great artists steal. - Pablo Picasso</li>
+        <li>\u00e7gua mole em pedra dura, tanto bate at\u008e que fura.</li>
+
+    </ul>
+  </body>
+</html>
+"""
+
+    def _assert_result_count(self, queries, html, count):
+        self.assertEquals(queries._parse_result_count(html), count)
+
+    def test_parse_result_count(self):
+        queries = BugzillaQueries(None)
+        # Pages with results, always list the count at least twice.
+        self._assert_result_count(queries, '<span class="bz_result_count">314 bugs found.</span><span class="bz_result_count">314 bugs found.</span>', 314)
+        self._assert_result_count(queries, '<span class="bz_result_count">Zarro Boogs found.</span>', 0)
+        self._assert_result_count(queries, '<span class="bz_result_count">\n \nOne bug found.</span>', 1)
+        self.assertRaises(Exception, queries._parse_result_count, ['Invalid'])
+
+    def test_request_page_parsing(self):
+        queries = BugzillaQueries(None)
+        self.assertEquals([40511, 40722, 40723], queries._parse_attachment_ids_request_query(self._sample_request_page))
+
+    def test_quip_page_parsing(self):
+        queries = BugzillaQueries(None)
+        expected_quips = ["Everything should be made as simple as possible, but not simpler. - Albert Einstein", "Good artists copy. Great artists steal. - Pablo Picasso", u"\u00e7gua mole em pedra dura, tanto bate at\u008e que fura."]
+        self.assertEquals(expected_quips, queries._parse_quips(self._sample_quip_page))
+
+    def test_load_query(self):
+        queries = BugzillaQueries(Mock())
+        queries._load_query("request.cgi?action=queue&type=review&group=type")
+
+
+class EditUsersParserTest(unittest.TestCase):
+    _example_user_results = """
+        <div id="bugzilla-body">
+        <p>1 user found.</p>
+        <table id="admin_table" border="1" cellpadding="4" cellspacing="0">
+          <tr bgcolor="#6666FF">
+              <th align="left">Edit user...
+              </th>
+              <th align="left">Real name
+              </th>
+              <th align="left">Account History
+              </th>
+          </tr>
+          <tr>
+              <td >
+                  <a href="editusers.cgi?action=edit&amp;userid=1234&amp;matchvalue=login_name&amp;groupid=&amp;grouprestrict=&amp;matchtype=substr&amp;matchstr=abarth%40webkit.org">
+                abarth&#64;webkit.org
+                  </a>
+              </td>
+              <td >
+                Adam Barth
+              </td>
+              <td >
+                  <a href="editusers.cgi?action=activity&amp;userid=1234&amp;matchvalue=login_name&amp;groupid=&amp;grouprestrict=&amp;matchtype=substr&amp;matchstr=abarth%40webkit.org">
+                View
+                  </a>
+              </td>
+          </tr>
+        </table>
+    """
+
+    _example_empty_user_results = """
+    <div id="bugzilla-body">
+    <p>0 users found.</p>
+    <table id="admin_table" border="1" cellpadding="4" cellspacing="0">
+      <tr bgcolor="#6666FF">
+          <th align="left">Edit user...
+          </th>
+          <th align="left">Real name
+          </th>
+          <th align="left">Account History
+          </th>
+      </tr>
+      <tr><td colspan="3" align="center"><i>&lt;none&gt;</i></td></tr>
+    </table>
+    """
+
+    def _assert_login_userid_pairs(self, results_page, expected_logins):
+        parser = EditUsersParser()
+        logins = parser.login_userid_pairs_from_edit_user_results(results_page)
+        self.assertEquals(logins, expected_logins)
+
+    def test_logins_from_editusers_results(self):
+        self._assert_login_userid_pairs(self._example_user_results, [("abarth@webkit.org", 1234)])
+        self._assert_login_userid_pairs(self._example_empty_user_results, [])
+
+    _example_user_page = """<table class="main"><tr>
+  <th><label for="login">Login name:</label></th>
+  <td>eric&#64;webkit.org
+  </td>
+</tr>
+<tr>
+  <th><label for="name">Real name:</label></th>
+  <td>Eric Seidel
+  </td>
+</tr>
+    <tr>
+      <th>Group access:</th>
+      <td>
+        <table class="groups">
+          <tr>
+          </tr>
+          <tr>
+            <th colspan="2">User is a member of these groups</th>
+          </tr>
+            <tr class="direct">
+              <td class="checkbox"><input type="checkbox"
+                           id="group_7"
+                           name="group_7"
+                           value="1" checked="checked" /></td>
+              <td class="groupname">
+                <label for="group_7">
+                  <strong>canconfirm:</strong>
+                  Can confirm a bug.
+                </label>
+              </td>
+            </tr>
+            <tr class="direct">
+              <td class="checkbox"><input type="checkbox"
+                           id="group_6"
+                           name="group_6"
+                           value="1" /></td>
+              <td class="groupname">
+                <label for="group_6">
+                  <strong>editbugs:</strong>
+                  Can edit all aspects of any bug.
+                /label>
+              </td>
+            </tr>
+        </table>
+      </td>
+    </tr>
+
+  <tr>
+    <th>Product responsibilities:</th>
+    <td>
+        <em>none</em>
+    </td>
+  </tr>
+</table>"""
+
+    def test_user_dict_from_edit_user_page(self):
+        parser = EditUsersParser()
+        user_dict = parser.user_dict_from_edit_user_page(self._example_user_page)
+        expected_user_dict = {u'login': u'eric@webkit.org', u'groups': set(['canconfirm']), u'name': u'Eric Seidel'}
+        self.assertEqual(expected_user_dict, user_dict)
diff --git a/Tools/Scripts/webkitpy/common/net/buildbot/__init__.py b/Tools/Scripts/webkitpy/common/net/buildbot/__init__.py
new file mode 100644
index 0000000..631ef6b
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/buildbot/__init__.py
@@ -0,0 +1,5 @@
+# Required for Python to search this directory for module files
+
+# We only export public API here.
+# It's unclear if Builder and Build need to be public.
+from .buildbot import BuildBot, Builder, Build
diff --git a/Tools/Scripts/webkitpy/common/net/buildbot/buildbot.py b/Tools/Scripts/webkitpy/common/net/buildbot/buildbot.py
new file mode 100644
index 0000000..adb5a3d
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/buildbot/buildbot.py
@@ -0,0 +1,492 @@
+# Copyright (c) 2009, 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 json
+import operator
+import re
+import urllib
+import urllib2
+
+import webkitpy.common.config.urls as config_urls
+from webkitpy.common.memoized import memoized
+from webkitpy.common.net.failuremap import FailureMap
+from webkitpy.common.net.layouttestresults import LayoutTestResults
+from webkitpy.common.net.networktransaction import NetworkTransaction
+from webkitpy.common.net.regressionwindow import RegressionWindow
+from webkitpy.common.system.logutils import get_logger
+from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup
+
+
+_log = get_logger(__file__)
+
+
+class Builder(object):
+    def __init__(self, name, buildbot):
+        self._name = name
+        self._buildbot = buildbot
+        self._builds_cache = {}
+        self._revision_to_build_number = None
+        from webkitpy.thirdparty.autoinstalled.mechanize import Browser
+        self._browser = Browser()
+        self._browser.set_handle_robots(False) # The builder pages are excluded by robots.txt
+
+    def name(self):
+        return self._name
+
+    def results_url(self):
+        return "%s/results/%s" % (self._buildbot.buildbot_url, self.url_encoded_name())
+
+    # In addition to per-build results, the build.chromium.org builders also
+    # keep a directory that accumulates test results over many runs.
+    def accumulated_results_url(self):
+        return None
+
+    def latest_layout_test_results_url(self):
+        return self.accumulated_results_url() or self.latest_cached_build().results_url();
+
+    @memoized
+    def latest_layout_test_results(self):
+        return self.fetch_layout_test_results(self.latest_layout_test_results_url())
+
+    def _fetch_file_from_results(self, results_url, file_name):
+        # It seems this can return None if the url redirects and then returns 404.
+        result = urllib2.urlopen("%s/%s" % (results_url, file_name))
+        if not result:
+            return None
+        # urlopen returns a file-like object which sometimes works fine with str()
+        # but sometimes is a addinfourl object.  In either case calling read() is correct.
+        return result.read()
+
+    def fetch_layout_test_results(self, results_url):
+        # FIXME: This should cache that the result was a 404 and stop hitting the network.
+        results_file = NetworkTransaction(convert_404_to_None=True).run(lambda: self._fetch_file_from_results(results_url, "full_results.json"))
+        if not results_file:
+            results_file = NetworkTransaction(convert_404_to_None=True).run(lambda: self._fetch_file_from_results(results_url, "results.html"))
+
+        # results_from_string accepts either ORWT html or NRWT json.
+        return LayoutTestResults.results_from_string(results_file)
+
+    def url_encoded_name(self):
+        return urllib.quote(self._name)
+
+    def url(self):
+        return "%s/builders/%s" % (self._buildbot.buildbot_url, self.url_encoded_name())
+
+    # This provides a single place to mock
+    def _fetch_build(self, build_number):
+        build_dictionary = self._buildbot._fetch_build_dictionary(self, build_number)
+        if not build_dictionary:
+            return None
+        revision_string = build_dictionary['sourceStamp']['revision']
+        return Build(self,
+            build_number=int(build_dictionary['number']),
+            # 'revision' may be None if a trunk build was started by the force-build button on the web page.
+            revision=(int(revision_string) if revision_string else None),
+            # Buildbot uses any nubmer other than 0 to mean fail.  Since we fetch with
+            # filter=1, passing builds may contain no 'results' value.
+            is_green=(not build_dictionary.get('results')),
+        )
+
+    def build(self, build_number):
+        if not build_number:
+            return None
+        cached_build = self._builds_cache.get(build_number)
+        if cached_build:
+            return cached_build
+
+        build = self._fetch_build(build_number)
+        self._builds_cache[build_number] = build
+        return build
+
+    def latest_cached_build(self):
+        revision_build_pairs = self.revision_build_pairs_with_results()
+        revision_build_pairs.sort(key=lambda i: i[1])
+        latest_build_number = revision_build_pairs[-1][1]
+        return self.build(latest_build_number)
+
+    def force_build(self, username="webkit-patch", comments=None):
+        def predicate(form):
+            try:
+                return form.find_control("username")
+            except Exception, e:
+                return False
+        self._browser.open(self.url())
+        self._browser.select_form(predicate=predicate)
+        self._browser["username"] = username
+        if comments:
+            self._browser["comments"] = comments
+        return self._browser.submit()
+
+    file_name_regexp = re.compile(r"r(?P<revision>\d+) \((?P<build_number>\d+)\)")
+    def _revision_and_build_for_filename(self, filename):
+        # Example: "r47483 (1)/" or "r47483 (1).zip"
+        match = self.file_name_regexp.match(filename)
+        if not match:
+            return None
+        return (int(match.group("revision")), int(match.group("build_number")))
+
+    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.
+            # Better would be to ask buildbot through some sort of API.
+            print "Loading revision/build list from %s." % self.results_url()
+            print "This may take a while..."
+            result_files = self._buildbot._fetch_twisted_directory_listing(self.results_url())
+        except urllib2.HTTPError, error:
+            if error.code != 404:
+                raise
+            _log.debug("Revision/build list failed to load.")
+            result_files = []
+        return dict(self._file_info_list_to_revision_to_build_list(result_files))
+
+    def _file_info_list_to_revision_to_build_list(self, file_info_list):
+        # This assumes there was only one build per revision, which is false but we don't care for now.
+        revisions_and_builds = []
+        for file_info in file_info_list:
+            revision_and_build = self._revision_and_build_for_filename(file_info["filename"])
+            if revision_and_build:
+                revisions_and_builds.append(revision_and_build)
+        return revisions_and_builds
+
+    def _revision_to_build_map(self):
+        if not self._revision_to_build_number:
+            self._revision_to_build_number = self._fetch_revision_to_build_map()
+        return self._revision_to_build_number
+
+    def revision_build_pairs_with_results(self):
+        return self._revision_to_build_map().items()
+
+    # This assumes there can be only one build per revision, which is false, but we don't care for now.
+    def build_for_revision(self, revision, allow_failed_lookups=False):
+        # NOTE: This lookup will fail if that exact revision was never built.
+        build_number = self._revision_to_build_map().get(int(revision))
+        if not build_number:
+            return None
+        build = self.build(build_number)
+        if not build and allow_failed_lookups:
+            # Builds for old revisions with fail to lookup via buildbot's json api.
+            build = Build(self,
+                build_number=build_number,
+                revision=revision,
+                is_green=False,
+            )
+        return build
+
+    def find_regression_window(self, red_build, look_back_limit=30):
+        if not red_build or red_build.is_green():
+            return RegressionWindow(None, None)
+        common_failures = None
+        current_build = red_build
+        build_after_current_build = None
+        look_back_count = 0
+        while current_build:
+            if current_build.is_green():
+                # current_build can't possibly have any failures in common
+                # with red_build because it's green.
+                break
+            results = current_build.layout_test_results()
+            # We treat a lack of results as if all the test failed.
+            # This occurs, for example, when we can't compile at all.
+            if results:
+                failures = set(results.failing_tests())
+                if common_failures == None:
+                    common_failures = failures
+                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, 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, 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)
+        regression_window = self.find_regression_window(red_build, look_back_limit)
+        if not regression_window.build_before_failure():
+            return None  # We ran off the limit of our search
+        # If avoid_flakey_tests, require at least 2 bad builds before we
+        # suspect a real failure transition.
+        if avoid_flakey_tests and regression_window.failing_build() == red_build:
+            return None
+        return regression_window
+
+
+class Build(object):
+    def __init__(self, builder, build_number, revision, is_green):
+        self._builder = builder
+        self._number = build_number
+        self._revision = revision
+        self._is_green = is_green
+
+    @staticmethod
+    def build_url(builder, build_number):
+        return "%s/builds/%s" % (builder.url(), build_number)
+
+    def url(self):
+        return self.build_url(self.builder(), self._number)
+
+    def results_url(self):
+        results_directory = "r%s (%s)" % (self.revision(), self._number)
+        return "%s/%s" % (self._builder.results_url(), urllib.quote(results_directory))
+
+    def results_zip_url(self):
+        return "%s.zip" % self.results_url()
+
+    @memoized
+    def layout_test_results(self):
+        return self._builder.fetch_layout_test_results(self.results_url())
+
+    def builder(self):
+        return self._builder
+
+    def revision(self):
+        return self._revision
+
+    def is_green(self):
+        return self._is_green
+
+    def previous_build(self):
+        # previous_build() allows callers to avoid assuming build numbers are sequential.
+        # They may not be sequential across all master changes, or when non-trunk builds are made.
+        return self._builder.build(self._number - 1)
+
+
+class BuildBot(object):
+    _builder_factory = Builder
+    _default_url = config_urls.buildbot_url
+
+    def __init__(self, url=None):
+        self.buildbot_url = url if url else self._default_url
+        self._builder_by_name = {}
+
+    def _parse_last_build_cell(self, builder, cell):
+        status_link = cell.find('a')
+        if status_link:
+            # Will be either a revision number or a build number
+            revision_string = status_link.string
+            # If revision_string has non-digits assume it's not a revision number.
+            builder['built_revision'] = int(revision_string) \
+                                        if not re.match('\D', revision_string) \
+                                        else None
+
+            # FIXME: We treat slave lost as green even though it is not to
+            # work around the Qts bot being on a broken internet connection.
+            # The real fix is https://bugs.webkit.org/show_bug.cgi?id=37099
+            builder['is_green'] = not re.search('fail', cell.renderContents()) or \
+                                  not not re.search('lost', cell.renderContents())
+
+            status_link_regexp = r"builders/(?P<builder_name>.*)/builds/(?P<build_number>\d+)"
+            link_match = re.match(status_link_regexp, status_link['href'])
+            builder['build_number'] = int(link_match.group("build_number"))
+        else:
+            # We failed to find a link in the first cell, just give up.  This
+            # can happen if a builder is just-added, the first cell will just
+            # be "no build"
+            # Other parts of the code depend on is_green being present.
+            builder['is_green'] = False
+            builder['built_revision'] = None
+            builder['build_number'] = None
+
+    def _parse_current_build_cell(self, builder, cell):
+        activity_lines = cell.renderContents().split("<br />")
+        builder["activity"] = activity_lines[0] # normally "building" or "idle"
+        # The middle lines document how long left for any current builds.
+        match = re.match("(?P<pending_builds>\d) pending", activity_lines[-1])
+        builder["pending_builds"] = int(match.group("pending_builds")) if match else 0
+
+    def _parse_builder_status_from_row(self, status_row):
+        status_cells = status_row.findAll('td')
+        builder = {}
+
+        # First cell is the name
+        name_link = status_cells[0].find('a')
+        builder["name"] = unicode(name_link.string)
+
+        self._parse_last_build_cell(builder, status_cells[1])
+        self._parse_current_build_cell(builder, status_cells[2])
+        return builder
+
+    def _matches_regexps(self, builder_name, name_regexps):
+        for name_regexp in name_regexps:
+            if re.match(name_regexp, builder_name):
+                return True
+        return False
+
+    # FIXME: This method needs to die, but is used by a unit test at the moment.
+    def _builder_statuses_with_names_matching_regexps(self, builder_statuses, name_regexps):
+        return [builder for builder in builder_statuses if self._matches_regexps(builder["name"], name_regexps)]
+
+    # FIXME: These _fetch methods should move to a networking class.
+    def _fetch_build_dictionary(self, builder, build_number):
+        # Note: filter=1 will remove None and {} and '', which cuts noise but can
+        # cause keys to be missing which you might otherwise expect.
+        # FIXME: The bot sends a *huge* amount of data for each request, we should
+        # find a way to reduce the response size further.
+        json_url = "%s/json/builders/%s/builds/%s?filter=1" % (self.buildbot_url, urllib.quote(builder.name()), build_number)
+        try:
+            return json.load(urllib2.urlopen(json_url))
+        except urllib2.URLError, err:
+            build_url = Build.build_url(builder, build_number)
+            _log.error("Error fetching data for %s build %s (%s, json: %s): %s" % (builder.name(), build_number, build_url, json_url, err))
+            return None
+        except ValueError, err:
+            build_url = Build.build_url(builder, build_number)
+            _log.error("Error decoding json data from %s: %s" % (build_url, err))
+            return None
+
+    def _fetch_one_box_per_builder(self):
+        build_status_url = "%s/one_box_per_builder" % self.buildbot_url
+        return urllib2.urlopen(build_status_url)
+
+    def _file_cell_text(self, file_cell):
+        """Traverses down through firstChild elements until one containing a string is found, then returns that string"""
+        element = file_cell
+        while element.string is None and element.contents:
+            element = element.contents[0]
+        return element.string
+
+    def _parse_twisted_file_row(self, file_row):
+        string_or_empty = lambda string: unicode(string) if string else u""
+        file_cells = file_row.findAll('td')
+        return {
+            "filename": string_or_empty(self._file_cell_text(file_cells[0])),
+            "size": string_or_empty(self._file_cell_text(file_cells[1])),
+            "type": string_or_empty(self._file_cell_text(file_cells[2])),
+            "encoding": string_or_empty(self._file_cell_text(file_cells[3])),
+        }
+
+    def _parse_twisted_directory_listing(self, page):
+        soup = BeautifulSoup(page)
+        # HACK: Match only table rows with a class to ignore twisted header/footer rows.
+        file_rows = soup.find('table').findAll('tr', {'class': re.compile(r'\b(?:directory|file)\b')})
+        return [self._parse_twisted_file_row(file_row) for file_row in file_rows]
+
+    # FIXME: There should be a better way to get this information directly from twisted.
+    def _fetch_twisted_directory_listing(self, url):
+        return self._parse_twisted_directory_listing(urllib2.urlopen(url))
+
+    def builders(self):
+        return [self.builder_with_name(status["name"]) for status in self.builder_statuses()]
+
+    # This method pulls from /one_box_per_builder as an efficient way to get information about
+    def builder_statuses(self):
+        soup = BeautifulSoup(self._fetch_one_box_per_builder())
+        return [self._parse_builder_status_from_row(status_row) for status_row in soup.find('table').findAll('tr')]
+
+    def builder_with_name(self, name):
+        builder = self._builder_by_name.get(name)
+        if not builder:
+            builder = self._builder_factory(name, self)
+            self._builder_by_name[name] = builder
+        return builder
+
+    def failure_map(self):
+        failure_map = FailureMap()
+        revision_to_failing_bots = {}
+        for builder_status in self.builder_statuses():
+            if builder_status["is_green"]:
+                continue
+            builder = self.builder_with_name(builder_status["name"])
+            regression_window = builder.find_blameworthy_regression_window(builder_status["build_number"])
+            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
+    # statuses in one request using self.builder_statuses (fetching /one_box_per_builder instead of builder pages).
+    def _latest_builds_from_builders(self):
+        builder_statuses = self.builder_statuses()
+        return [self.builder_with_name(status["name"]).build(status["build_number"]) for status in builder_statuses]
+
+    def _build_at_or_before_revision(self, build, revision):
+        while build:
+            if build.revision() <= revision:
+                return build
+            build = build.previous_build()
+
+    def _fetch_builder_page(self, builder):
+        builder_page_url = "%s/builders/%s?numbuilds=100" % (self.buildbot_url, urllib2.quote(builder.name()))
+        return urllib2.urlopen(builder_page_url)
+
+    def _revisions_for_builder(self, builder):
+        soup = BeautifulSoup(self._fetch_builder_page(builder))
+        revisions = []
+        for status_row in soup.find('table').findAll('tr'):
+            revision_anchor = status_row.find('a')
+            table_cells = status_row.findAll('td')
+            if not table_cells or len(table_cells) < 3 or not table_cells[2].string:
+                continue
+            if revision_anchor and revision_anchor.string and re.match(r'^\d+$', revision_anchor.string):
+                revisions.append((int(revision_anchor.string), 'success' in table_cells[2].string))
+        return revisions
+
+    def _find_green_revision(self, builder_revisions):
+        revision_statuses = {}
+        for builder in builder_revisions:
+            for revision, succeeded in builder_revisions[builder]:
+                revision_statuses.setdefault(revision, set())
+                if succeeded and revision_statuses[revision] != None:
+                    revision_statuses[revision].add(builder)
+                else:
+                    revision_statuses[revision] = None
+
+        # In descending order, look for a revision X with successful builds
+        # Once we found X, check if remaining builders succeeded in the neighborhood of X.
+        revisions_in_order = sorted(revision_statuses.keys(), reverse=True)
+        for i, revision in enumerate(revisions_in_order):
+            if not revision_statuses[revision]:
+                continue
+
+            builders_succeeded_in_future = set()
+            for future_revision in sorted(revisions_in_order[:i + 1]):
+                if not revision_statuses[future_revision]:
+                    break
+                builders_succeeded_in_future = builders_succeeded_in_future.union(revision_statuses[future_revision])
+
+            builders_succeeded_in_past = set()
+            for past_revision in revisions_in_order[i:]:
+                if not revision_statuses[past_revision]:
+                    break
+                builders_succeeded_in_past = builders_succeeded_in_past.union(revision_statuses[past_revision])
+
+            if len(builders_succeeded_in_future) == len(builder_revisions) and len(builders_succeeded_in_past) == len(builder_revisions):
+                return revision
+        return None
diff --git a/Tools/Scripts/webkitpy/common/net/buildbot/buildbot_mock.py b/Tools/Scripts/webkitpy/common/net/buildbot/buildbot_mock.py
new file mode 100644
index 0000000..f8ec49b
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/buildbot/buildbot_mock.py
@@ -0,0 +1,109 @@
+# Copyright (C) 2011 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 MockBuild(object):
+    def __init__(self, build_number, revision, is_green):
+        self._number = build_number
+        self._revision = revision
+        self._is_green = is_green
+
+class MockBuilder(object):
+    def __init__(self, name):
+        self._name = name
+
+    def name(self):
+        return self._name
+
+    def build(self, build_number):
+        return MockBuild(build_number=build_number, revision=1234, is_green=False)
+
+    def results_url(self):
+        return "http://example.com/builders/%s/results" % self.name()
+
+    def accumulated_results_url(self):
+        return "http://example.com/f/builders/%s/results/layout-test-results" % self.name()
+
+    def latest_layout_test_results_url(self):
+        return self.accumulated_results_url()
+
+    def force_build(self, username, comments):
+        log("MOCK: force_build: name=%s, username=%s, comments=%s" % (
+            self._name, username, comments))
+
+
+class MockFailureMap(object):
+    def __init__(self, buildbot):
+        self._buildbot = buildbot
+
+    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"]
+
+    def failing_tests(self):
+        return set(["mock-test-1"])
+
+
+class MockBuildBot(object):
+    def __init__(self):
+        self._mock_builder1_status = {
+            "name": "Builder1",
+            "is_green": True,
+            "activity": "building",
+        }
+        self._mock_builder2_status = {
+            "name": "Builder2",
+            "is_green": True,
+            "activity": "idle",
+        }
+
+    def builder_with_name(self, name):
+        return MockBuilder(name)
+
+    def builder_statuses(self):
+        return [
+            self._mock_builder1_status,
+            self._mock_builder2_status,
+        ]
+
+    def light_tree_on_fire(self):
+        self._mock_builder2_status["is_green"] = False
+
+    def failure_map(self):
+        return MockFailureMap(self)
diff --git a/Tools/Scripts/webkitpy/common/net/buildbot/buildbot_unittest.py b/Tools/Scripts/webkitpy/common/net/buildbot/buildbot_unittest.py
new file mode 100644
index 0000000..69f8648
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/buildbot/buildbot_unittest.py
@@ -0,0 +1,479 @@
+# Copyright (C) 2009 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
+from webkitpy.common.net.buildbot import BuildBot, Builder, Build
+from webkitpy.layout_tests.models import test_results
+from webkitpy.layout_tests.models import test_failures
+from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup
+
+
+class BuilderTest(unittest.TestCase):
+    def _mock_test_result(self, testname):
+        return test_results.TestResult(testname, [test_failures.FailureTextMismatch()])
+
+    def _install_fetch_build(self, failure):
+        def _mock_fetch_build(build_number):
+            build = Build(
+                builder=self.builder,
+                build_number=build_number,
+                revision=build_number + 1000,
+                is_green=build_number < 4
+            )
+            results = [self._mock_test_result(testname) for testname in failure(build_number)]
+            layout_test_results = LayoutTestResults(results)
+            def mock_layout_test_results():
+                return layout_test_results
+            build.layout_test_results = mock_layout_test_results
+            return build
+        self.builder._fetch_build = _mock_fetch_build
+
+    def setUp(self):
+        self.buildbot = BuildBot()
+        self.builder = Builder(u"Test Builder \u2661", self.buildbot)
+        self._install_fetch_build(lambda build_number: ["test1", "test2"])
+
+    def test_latest_layout_test_results(self):
+        self.builder.fetch_layout_test_results = lambda results_url: LayoutTestResults([self._mock_test_result(testname) for testname in ["test1", "test2"]])
+        self.builder.accumulated_results_url = lambda: "http://dummy_url.org"
+        self.assertTrue(self.builder.latest_layout_test_results())
+
+    def test_find_regression_window(self):
+        regression_window = self.builder.find_regression_window(self.builder.build(10))
+        self.assertEqual(regression_window.build_before_failure().revision(), 1003)
+        self.assertEqual(regression_window.failing_build().revision(), 1004)
+
+        regression_window = self.builder.find_regression_window(self.builder.build(10), look_back_limit=2)
+        self.assertEqual(regression_window.build_before_failure(), None)
+        self.assertEqual(regression_window.failing_build().revision(), 1008)
+
+    def test_none_build(self):
+        self.builder._fetch_build = lambda build_number: None
+        regression_window = self.builder.find_regression_window(self.builder.build(10))
+        self.assertEqual(regression_window.build_before_failure(), None)
+        self.assertEqual(regression_window.failing_build(), None)
+
+    def test_flaky_tests(self):
+        self._install_fetch_build(lambda build_number: ["test1"] if build_number % 2 else ["test2"])
+        regression_window = self.builder.find_regression_window(self.builder.build(10))
+        self.assertEqual(regression_window.build_before_failure().revision(), 1009)
+        self.assertEqual(regression_window.failing_build().revision(), 1010)
+
+    def test_failure_and_flaky(self):
+        self._install_fetch_build(lambda build_number: ["test1", "test2"] if build_number % 2 else ["test2"])
+        regression_window = self.builder.find_regression_window(self.builder.build(10))
+        self.assertEqual(regression_window.build_before_failure().revision(), 1003)
+        self.assertEqual(regression_window.failing_build().revision(), 1004)
+
+    def test_no_results(self):
+        self._install_fetch_build(lambda build_number: ["test1", "test2"] if build_number % 2 else ["test2"])
+        regression_window = self.builder.find_regression_window(self.builder.build(10))
+        self.assertEqual(regression_window.build_before_failure().revision(), 1003)
+        self.assertEqual(regression_window.failing_build().revision(), 1004)
+
+    def test_failure_after_flaky(self):
+        self._install_fetch_build(lambda build_number: ["test1", "test2"] if build_number > 6 else ["test3"])
+        regression_window = self.builder.find_regression_window(self.builder.build(10))
+        self.assertEqual(regression_window.build_before_failure().revision(), 1006)
+        self.assertEqual(regression_window.failing_build().revision(), 1007)
+
+    def test_find_blameworthy_regression_window(self):
+        self.assertEqual(self.builder.find_blameworthy_regression_window(10).revisions(), [1004])
+        self.assertEqual(self.builder.find_blameworthy_regression_window(10, look_back_limit=2), None)
+        # Flakey test avoidance requires at least 2 red builds:
+        self.assertEqual(self.builder.find_blameworthy_regression_window(4), None)
+        self.assertEqual(self.builder.find_blameworthy_regression_window(4, avoid_flakey_tests=False).revisions(), [1004])
+        # Green builder:
+        self.assertEqual(self.builder.find_blameworthy_regression_window(3), None)
+
+    def test_build_caching(self):
+        self.assertEqual(self.builder.build(10), self.builder.build(10))
+
+    def test_build_and_revision_for_filename(self):
+        expectations = {
+            "r47483 (1)/" : (47483, 1),
+            "r47483 (1).zip" : (47483, 1),
+            "random junk": None,
+        }
+        for filename, revision_and_build in expectations.items():
+            self.assertEqual(self.builder._revision_and_build_for_filename(filename), revision_and_build)
+
+    def test_file_info_list_to_revision_to_build_list(self):
+        file_info_list = [
+            {"filename": "r47483 (1)/"},
+            {"filename": "r47483 (1).zip"},
+            {"filename": "random junk"},
+        ]
+        builds_and_revisions_list = [(47483, 1), (47483, 1)]
+        self.assertEqual(self.builder._file_info_list_to_revision_to_build_list(file_info_list), builds_and_revisions_list)
+
+    def test_fetch_build(self):
+        buildbot = BuildBot()
+        builder = Builder(u"Test Builder \u2661", buildbot)
+
+        def mock_fetch_build_dictionary(self, build_number):
+            build_dictionary = {
+                "sourceStamp": {
+                    "revision": None,  # revision=None means a trunk build started from the force-build button on the builder page.
+                    },
+                "number": int(build_number),
+                # Intentionally missing the 'results' key, meaning it's a "pass" build.
+            }
+            return build_dictionary
+        buildbot._fetch_build_dictionary = mock_fetch_build_dictionary
+        self.assertNotEqual(builder._fetch_build(1), None)
+
+
+class BuildTest(unittest.TestCase):
+    def test_layout_test_results(self):
+        buildbot = BuildBot()
+        builder = Builder(u"Foo Builder (test)", buildbot)
+        builder._fetch_file_from_results = lambda results_url, file_name: None
+        build = Build(builder, None, None, None)
+        # Test that layout_test_results() returns None if the fetch fails.
+        self.assertEqual(build.layout_test_results(), None)
+
+
+class BuildBotTest(unittest.TestCase):
+
+    _example_one_box_status = '''
+    <table>
+    <tr>
+    <td class="box"><a href="builders/Windows%20Debug%20%28Tests%29">Windows Debug (Tests)</a></td>
+      <td align="center" class="LastBuild box success"><a href="builders/Windows%20Debug%20%28Tests%29/builds/3693">47380</a><br />build<br />successful</td>
+      <td align="center" class="Activity building">building<br />ETA in<br />~ 14 mins<br />at 13:40</td>
+    <tr>
+    <td class="box"><a href="builders/SnowLeopard%20Intel%20Release">SnowLeopard Intel Release</a></td>
+      <td class="LastBuild box" >no build</td>
+      <td align="center" class="Activity building">building<br />< 1 min</td>
+    <tr>
+    <td class="box"><a href="builders/Qt%20Linux%20Release">Qt Linux Release</a></td>
+      <td align="center" class="LastBuild box failure"><a href="builders/Qt%20Linux%20Release/builds/654">47383</a><br />failed<br />compile-webkit</td>
+      <td align="center" class="Activity idle">idle<br />3 pending</td>
+    <tr>
+    <td class="box"><a href="builders/Qt%20Windows%2032-bit%20Debug">Qt Windows 32-bit Debug</a></td>
+      <td align="center" class="LastBuild box failure"><a href="builders/Qt%20Windows%2032-bit%20Debug/builds/2090">60563</a><br />failed<br />failed<br />slave<br />lost</td>
+      <td align="center" class="Activity building">building<br />ETA in<br />~ 5 mins<br />at 08:25</td>
+    </table>
+'''
+    _expected_example_one_box_parsings = [
+        {
+            'is_green': True,
+            'build_number' : 3693,
+            'name': u'Windows Debug (Tests)',
+            'built_revision': 47380,
+            'activity': 'building',
+            'pending_builds': 0,
+        },
+        {
+            'is_green': False,
+            'build_number' : None,
+            'name': u'SnowLeopard Intel Release',
+            'built_revision': None,
+            'activity': 'building',
+            'pending_builds': 0,
+        },
+        {
+            'is_green': False,
+            'build_number' : 654,
+            'name': u'Qt Linux Release',
+            'built_revision': 47383,
+            'activity': 'idle',
+            'pending_builds': 3,
+        },
+        {
+            'is_green': True,
+            'build_number' : 2090,
+            'name': u'Qt Windows 32-bit Debug',
+            'built_revision': 60563,
+            'activity': 'building',
+            'pending_builds': 0,
+        },
+    ]
+
+    def test_status_parsing(self):
+        buildbot = BuildBot()
+
+        soup = BeautifulSoup(self._example_one_box_status)
+        status_table = soup.find("table")
+        input_rows = status_table.findAll('tr')
+
+        for x in range(len(input_rows)):
+            status_row = input_rows[x]
+            expected_parsing = self._expected_example_one_box_parsings[x]
+
+            builder = buildbot._parse_builder_status_from_row(status_row)
+
+            # Make sure we aren't parsing more or less than we expect
+            self.assertEquals(builder.keys(), expected_parsing.keys())
+
+            for key, expected_value in expected_parsing.items():
+                self.assertEquals(builder[key], expected_value, ("Builder %d parse failure for key: %s: Actual='%s' Expected='%s'" % (x, key, builder[key], expected_value)))
+
+    def test_builder_with_name(self):
+        buildbot = BuildBot()
+
+        builder = buildbot.builder_with_name("Test Builder")
+        self.assertEqual(builder.name(), "Test Builder")
+        self.assertEqual(builder.url(), "http://build.webkit.org/builders/Test%20Builder")
+        self.assertEqual(builder.url_encoded_name(), "Test%20Builder")
+        self.assertEqual(builder.results_url(), "http://build.webkit.org/results/Test%20Builder")
+
+        # Override _fetch_build_dictionary function to not touch the network.
+        def mock_fetch_build_dictionary(self, build_number):
+            build_dictionary = {
+                "sourceStamp": {
+                    "revision" : 2 * build_number,
+                    },
+                "number" : int(build_number),
+                "results" : build_number % 2, # 0 means pass
+            }
+            return build_dictionary
+        buildbot._fetch_build_dictionary = mock_fetch_build_dictionary
+
+        build = builder.build(10)
+        self.assertEqual(build.builder(), builder)
+        self.assertEqual(build.url(), "http://build.webkit.org/builders/Test%20Builder/builds/10")
+        self.assertEqual(build.results_url(), "http://build.webkit.org/results/Test%20Builder/r20%20%2810%29")
+        self.assertEqual(build.revision(), 20)
+        self.assertEqual(build.is_green(), True)
+
+        build = build.previous_build()
+        self.assertEqual(build.builder(), builder)
+        self.assertEqual(build.url(), "http://build.webkit.org/builders/Test%20Builder/builds/9")
+        self.assertEqual(build.results_url(), "http://build.webkit.org/results/Test%20Builder/r18%20%289%29")
+        self.assertEqual(build.revision(), 18)
+        self.assertEqual(build.is_green(), False)
+
+        self.assertEqual(builder.build(None), None)
+
+    _example_directory_listing = '''
+<h1>Directory listing for /results/SnowLeopard Intel Leaks/</h1>
+
+<table>
+        <tr class="alt">
+            <th>Filename</th>
+            <th>Size</th>
+            <th>Content type</th>
+            <th>Content encoding</th>
+        </tr>
+<tr class="directory ">
+    <td><a href="r47483%20%281%29/"><b>r47483 (1)/</b></a></td>
+    <td><b></b></td>
+    <td><b>[Directory]</b></td>
+    <td><b></b></td>
+</tr>
+<tr class="file alt">
+    <td><a href="r47484%20%282%29.zip">r47484 (2).zip</a></td>
+    <td>89K</td>
+    <td>[application/zip]</td>
+    <td></td>
+</tr>
+'''
+    _expected_files = [
+        {
+            "filename" : "r47483 (1)/",
+            "size" : "",
+            "type" : "[Directory]",
+            "encoding" : "",
+        },
+        {
+            "filename" : "r47484 (2).zip",
+            "size" : "89K",
+            "type" : "[application/zip]",
+            "encoding" : "",
+        },
+    ]
+
+    def test_parse_build_to_revision_map(self):
+        buildbot = BuildBot()
+        files = buildbot._parse_twisted_directory_listing(self._example_directory_listing)
+        self.assertEqual(self._expected_files, files)
+
+    _fake_builder_page = '''
+    <body>
+    <div class="content">
+    <h1>Some Builder</h1>
+    <p>(<a href="../waterfall?show=Some Builder">view in waterfall</a>)</p>
+    <div class="column">
+    <h2>Recent Builds:</h2>
+    <table class="info">
+      <tr>
+        <th>Time</th>
+        <th>Revision</th>
+        <th>Result</th>    <th>Build #</th>
+        <th>Info</th>
+      </tr>
+      <tr class="alt">
+        <td>Jan 10 15:49</td>
+        <td><span class="revision" title="Revision 104643"><a href="http://trac.webkit.org/changeset/104643">104643</a></span></td>
+        <td class="success">failure</td>    <td><a href=".../37604">#37604</a></td>
+        <td class="left">Build successful</td>
+      </tr>
+      <tr class="">
+        <td>Jan 10 15:32</td>
+        <td><span class="revision" title="Revision 104636"><a href="http://trac.webkit.org/changeset/104636">104636</a></span></td>
+        <td class="success">failure</td>    <td><a href=".../37603">#37603</a></td>
+        <td class="left">Build successful</td>
+      </tr>
+      <tr class="alt">
+        <td>Jan 10 15:18</td>
+        <td><span class="revision" title="Revision 104635"><a href="http://trac.webkit.org/changeset/104635">104635</a></span></td>
+        <td class="success">success</td>    <td><a href=".../37602">#37602</a></td>
+        <td class="left">Build successful</td>
+      </tr>
+      <tr class="">
+        <td>Jan 10 14:51</td>
+        <td><span class="revision" title="Revision 104633"><a href="http://trac.webkit.org/changeset/104633">104633</a></span></td>
+        <td class="failure">failure</td>    <td><a href=".../37601">#37601</a></td>
+        <td class="left">Failed compile-webkit</td>
+      </tr>
+    </table>
+    </body>'''
+    _fake_builder_page_without_success = '''
+    <body>
+    <table>
+      <tr class="alt">
+        <td>Jan 10 15:49</td>
+        <td><span class="revision" title="Revision 104643"><a href="http://trac.webkit.org/changeset/104643">104643</a></span></td>
+        <td class="success">failure</td>
+      </tr>
+      <tr class="">
+        <td>Jan 10 15:32</td>
+        <td><span class="revision" title="Revision 104636"><a href="http://trac.webkit.org/changeset/104636">104636</a></span></td>
+        <td class="success">failure</td>
+      </tr>
+      <tr class="alt">
+        <td>Jan 10 15:18</td>
+        <td><span class="revision" title="Revision 104635"><a href="http://trac.webkit.org/changeset/104635">104635</a></span></td>
+        <td class="success">failure</td>
+      </tr>
+      <tr class="">
+          <td>Jan 10 11:58</td>
+          <td><span class="revision" title="Revision ??"><a href="http://trac.webkit.org/changeset/%3F%3F">??</a></span></td>
+          <td class="retry">retry</td>
+        </tr>
+      <tr class="">
+        <td>Jan 10 14:51</td>
+        <td><span class="revision" title="Revision 104633"><a href="http://trac.webkit.org/changeset/104633">104633</a></span></td>
+        <td class="failure">failure</td>
+      </tr>
+    </table>
+    </body>'''
+
+    def test_revisions_for_builder(self):
+        buildbot = BuildBot()
+        buildbot._fetch_builder_page = lambda builder: builder.page
+        builder_with_success = Builder('Some builder', None)
+        builder_with_success.page = self._fake_builder_page
+        self.assertEqual(buildbot._revisions_for_builder(builder_with_success), [(104643, False), (104636, False), (104635, True), (104633, False)])
+
+        builder_without_success = Builder('Some builder', None)
+        builder_without_success.page = self._fake_builder_page_without_success
+        self.assertEqual(buildbot._revisions_for_builder(builder_without_success), [(104643, False), (104636, False), (104635, False), (104633, False)])
+
+    def test_find_green_revision(self):
+        buildbot = BuildBot()
+        self.assertEqual(buildbot._find_green_revision({
+            'Builder 1': [(1, True), (3, True)],
+            'Builder 2': [(1, True), (3, False)],
+            'Builder 3': [(1, True), (3, True)],
+        }), 1)
+        self.assertEqual(buildbot._find_green_revision({
+            'Builder 1': [(1, False), (3, True)],
+            'Builder 2': [(1, True), (3, True)],
+            'Builder 3': [(1, True), (3, True)],
+        }), 3)
+        self.assertEqual(buildbot._find_green_revision({
+            'Builder 1': [(1, True), (2, True)],
+            'Builder 2': [(1, False), (2, True), (3, True)],
+            'Builder 3': [(1, True), (3, True)],
+        }), None)
+        self.assertEqual(buildbot._find_green_revision({
+            'Builder 1': [(1, True), (2, True)],
+            'Builder 2': [(1, True), (2, True), (3, True)],
+            'Builder 3': [(1, True), (3, True)],
+        }), 2)
+        self.assertEqual(buildbot._find_green_revision({
+            'Builder 1': [(1, False), (2, True)],
+            'Builder 2': [(1, True), (3, True)],
+            'Builder 3': [(1, True), (3, True)],
+        }), None)
+        self.assertEqual(buildbot._find_green_revision({
+            'Builder 1': [(1, True), (3, True)],
+            'Builder 2': [(1, False), (2, True), (3, True), (4, True)],
+            'Builder 3': [(2, True), (4, True)],
+        }), 3)
+        self.assertEqual(buildbot._find_green_revision({
+            'Builder 1': [(1, True), (3, True)],
+            'Builder 2': [(1, False), (2, True), (3, True), (4, False)],
+            'Builder 3': [(2, True), (4, True)],
+        }), None)
+        self.assertEqual(buildbot._find_green_revision({
+            'Builder 1': [(1, True), (3, True)],
+            'Builder 2': [(1, False), (2, True), (3, True), (4, False)],
+            'Builder 3': [(2, True), (3, True), (4, True)],
+        }), 3)
+        self.assertEqual(buildbot._find_green_revision({
+            'Builder 1': [(1, True), (2, True)],
+            'Builder 2': [],
+            'Builder 3': [(1, True), (2, True)],
+        }), None)
+        self.assertEqual(buildbot._find_green_revision({
+            'Builder 1': [(1, True), (3, False), (5, True), (10, True), (12, False)],
+            'Builder 2': [(1, True), (3, False), (7, True), (9, True), (12, False)],
+            'Builder 3': [(1, True), (3, True), (7, True), (11, False), (12, True)],
+        }), 7)
+
+    def _fetch_build(self, build_number):
+        if build_number == 5:
+            return "correct build"
+        return "wrong build"
+
+    def _fetch_revision_to_build_map(self):
+        return {'r5': 5, 'r2': 2, 'r3': 3}
+
+    def test_latest_cached_build(self):
+        b = Builder('builder', BuildBot())
+        b._fetch_build = self._fetch_build
+        b._fetch_revision_to_build_map = self._fetch_revision_to_build_map
+        self.assertEquals("correct build", b.latest_cached_build())
+
+    def results_url(self):
+        return "some-url"
+
+    def test_results_zip_url(self):
+        b = Build(None, 123, 123, False)
+        b.results_url = self.results_url
+        self.assertEquals("some-url.zip", b.results_zip_url())
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/common/net/buildbot/chromiumbuildbot.py b/Tools/Scripts/webkitpy/common/net/buildbot/chromiumbuildbot.py
new file mode 100644
index 0000000..5030bba
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/buildbot/chromiumbuildbot.py
@@ -0,0 +1,47 @@
+# Copyright (c) 2011, 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 webkitpy.common.config.urls as config_urls
+from webkitpy.common.net.buildbot.buildbot import Builder, BuildBot
+# FIXME: builders should probably be in webkitpy.common.config.
+from webkitpy.layout_tests.port.builders import builder_path_from_name
+
+
+class ChromiumBuilder(Builder):
+    # The build.chromium.org builders store their results in a different
+    # location than the build.webkit.org builders.
+    def results_url(self):
+        return "http://build.chromium.org/f/chromium/layout_test_results/%s" % builder_path_from_name(self._name)
+
+    def accumulated_results_url(self):
+        return self.results_url() + "/results/layout-test-results"
+
+
+class ChromiumBuildBot(BuildBot):
+    _builder_factory = ChromiumBuilder
+    _default_url = config_urls.chromium_buildbot_url
diff --git a/Tools/Scripts/webkitpy/common/net/credentials.py b/Tools/Scripts/webkitpy/common/net/credentials.py
new file mode 100644
index 0000000..21aeaea
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/credentials.py
@@ -0,0 +1,161 @@
+# Copyright (c) 2009 Google Inc. All rights reserved.
+# Copyright (c) 2009 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:
+#
+#     * 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.
+#
+# Python module for reading stored web credentials from the OS.
+
+import os
+import platform
+import re
+
+from webkitpy.common.checkout.scm import Git
+from webkitpy.common.system.executive import Executive, ScriptError
+from webkitpy.common.system.user import User
+from webkitpy.common.system.deprecated_logging import log
+
+try:
+    # Use keyring, a cross platform keyring interface, as a fallback:
+    # http://pypi.python.org/pypi/keyring
+    import keyring
+except ImportError:
+    keyring = None
+
+
+class Credentials(object):
+    _environ_prefix = "webkit_bugzilla_"
+
+    def __init__(self, host, git_prefix=None, executive=None, cwd=os.getcwd(),
+                 keyring=keyring):
+        self.host = host
+        self.git_prefix = "%s." % git_prefix if git_prefix else ""
+        self.executive = executive or Executive()
+        self.cwd = cwd
+        self._keyring = keyring
+
+    def _credentials_from_git(self):
+        try:
+            if not Git.in_working_directory(self.cwd):
+                return (None, None)
+            return (Git.read_git_config(self.git_prefix + "username"),
+                    Git.read_git_config(self.git_prefix + "password"))
+        except OSError, e:
+            # Catch and ignore OSError exceptions such as "no such file
+            # or directory" (OSError errno 2), which imply that the Git
+            # command cannot be found/is not installed.
+            pass
+        return (None, None)
+
+    def _keychain_value_with_label(self, label, source_text):
+        match = re.search("%s\"(?P<value>.+)\"" % label,
+                                                  source_text,
+                                                  re.MULTILINE)
+        if match:
+            return match.group('value')
+
+    def _is_mac_os_x(self):
+        return platform.mac_ver()[0]
+
+    def _parse_security_tool_output(self, security_output):
+        username = self._keychain_value_with_label("^\s*\"acct\"<blob>=",
+                                                   security_output)
+        password = self._keychain_value_with_label("^password: ",
+                                                   security_output)
+        return [username, password]
+
+    def _run_security_tool(self, username=None):
+        security_command = [
+            "/usr/bin/security",
+            "find-internet-password",
+            "-g",
+            "-s",
+            self.host,
+        ]
+        if username:
+            security_command += ["-a", username]
+
+        log("Reading Keychain for %s account and password.  "
+            "Click \"Allow\" to continue..." % self.host)
+        try:
+            return self.executive.run_command(security_command)
+        except ScriptError:
+            # Failed to either find a keychain entry or somekind of OS-related
+            # error occured (for instance, couldn't find the /usr/sbin/security
+            # command).
+            log("Could not find a keychain entry for %s." % self.host)
+            return None
+
+    def _credentials_from_keychain(self, username=None):
+        if not self._is_mac_os_x():
+            return [username, None]
+
+        security_output = self._run_security_tool(username)
+        if security_output:
+            return self._parse_security_tool_output(security_output)
+        else:
+            return [None, None]
+
+    def _read_environ(self, key):
+        environ_key = self._environ_prefix + key
+        return os.environ.get(environ_key.upper())
+
+    def _credentials_from_environment(self):
+        return (self._read_environ("username"), self._read_environ("password"))
+
+    def _offer_to_store_credentials_in_keyring(self, username, password):
+        if not self._keyring:
+            return
+        if not User().confirm("Store password in system keyring?", User.DEFAULT_NO):
+            return
+        try:
+            self._keyring.set_password(self.host, username, password)
+        except:
+            pass
+
+    def read_credentials(self, user=User):
+        username, password = self._credentials_from_environment()
+        # FIXME: We don't currently support pulling the username from one
+        # source and the password from a separate source.
+        if not username or not password:
+            username, password = self._credentials_from_git()
+        if not username or not password:
+            username, password = self._credentials_from_keychain(username)
+
+        if not username:
+            username = user.prompt("%s login: " % self.host)
+
+        if username and not password and self._keyring:
+            try:
+                password = self._keyring.get_password(self.host, username)
+            except:
+                pass
+
+        if not password:
+            password = user.prompt_password("%s password for %s: " % (self.host, username))
+            self._offer_to_store_credentials_in_keyring(username, password)
+
+        return (username, password)
diff --git a/Tools/Scripts/webkitpy/common/net/credentials_unittest.py b/Tools/Scripts/webkitpy/common/net/credentials_unittest.py
new file mode 100644
index 0000000..a797e3d
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/credentials_unittest.py
@@ -0,0 +1,212 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import tempfile
+import unittest
+from webkitpy.common.net.credentials import Credentials
+from webkitpy.common.system.executive import Executive
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.system.user_mock import MockUser
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.mocktool import MockOptions
+from webkitpy.common.system.executive_mock import MockExecutive
+
+
+# FIXME: Other unit tests probably want this class.
+class _TemporaryDirectory(object):
+    def __init__(self, **kwargs):
+        self._kwargs = kwargs
+        self._directory_path = None
+
+    def __enter__(self):
+        self._directory_path = tempfile.mkdtemp(**self._kwargs)
+        return self._directory_path
+
+    def __exit__(self, type, value, traceback):
+        os.rmdir(self._directory_path)
+
+
+# Note: All tests should use this class instead of Credentials directly to avoid using a real Executive.
+class MockedCredentials(Credentials):
+    def __init__(self, *args, **kwargs):
+        if 'executive' not in kwargs:
+            kwargs['executive'] = MockExecutive()
+        Credentials.__init__(self, *args, **kwargs)
+
+
+class CredentialsTest(unittest.TestCase):
+    example_security_output = """keychain: "/Users/test/Library/Keychains/login.keychain"
+class: "inet"
+attributes:
+    0x00000007 <blob>="bugs.webkit.org (test@webkit.org)"
+    0x00000008 <blob>=<NULL>
+    "acct"<blob>="test@webkit.org"
+    "atyp"<blob>="form"
+    "cdat"<timedate>=0x32303039303832353233353231365A00  "20090825235216Z\000"
+    "crtr"<uint32>=<NULL>
+    "cusi"<sint32>=<NULL>
+    "desc"<blob>="Web form password"
+    "icmt"<blob>="default"
+    "invi"<sint32>=<NULL>
+    "mdat"<timedate>=0x32303039303930393137323635315A00  "20090909172651Z\000"
+    "nega"<sint32>=<NULL>
+    "path"<blob>=<NULL>
+    "port"<uint32>=0x00000000 
+    "prot"<blob>=<NULL>
+    "ptcl"<uint32>="htps"
+    "scrp"<sint32>=<NULL>
+    "sdmn"<blob>=<NULL>
+    "srvr"<blob>="bugs.webkit.org"
+    "type"<uint32>=<NULL>
+password: "SECRETSAUCE"
+"""
+
+    def test_keychain_lookup_on_non_mac(self):
+        class FakeCredentials(MockedCredentials):
+            def _is_mac_os_x(self):
+                return False
+        credentials = FakeCredentials("bugs.webkit.org")
+        self.assertEqual(credentials._is_mac_os_x(), False)
+        self.assertEqual(credentials._credentials_from_keychain("foo"), ["foo", None])
+
+    def test_security_output_parse(self):
+        credentials = MockedCredentials("bugs.webkit.org")
+        self.assertEqual(credentials._parse_security_tool_output(self.example_security_output), ["test@webkit.org", "SECRETSAUCE"])
+
+    def test_security_output_parse_entry_not_found(self):
+        # FIXME: This test won't work if the user has a credential for foo.example.com!
+        credentials = Credentials("foo.example.com")
+        if not credentials._is_mac_os_x():
+            return # This test does not run on a non-Mac.
+
+        # Note, we ignore the captured output because it is already covered
+        # by the test case CredentialsTest._assert_security_call (below).
+        outputCapture = OutputCapture()
+        outputCapture.capture_output()
+        self.assertEqual(credentials._run_security_tool(), None)
+        outputCapture.restore_output()
+
+    def _assert_security_call(self, username=None):
+        executive_mock = Mock()
+        credentials = MockedCredentials("example.com", executive=executive_mock)
+
+        expected_stderr = "Reading Keychain for example.com account and password.  Click \"Allow\" to continue...\n"
+        OutputCapture().assert_outputs(self, credentials._run_security_tool, [username], expected_stderr=expected_stderr)
+
+        security_args = ["/usr/bin/security", "find-internet-password", "-g", "-s", "example.com"]
+        if username:
+            security_args += ["-a", username]
+        executive_mock.run_command.assert_called_with(security_args)
+
+    def test_security_calls(self):
+        self._assert_security_call()
+        self._assert_security_call(username="foo")
+
+    def test_credentials_from_environment(self):
+        credentials = MockedCredentials("example.com")
+
+        saved_environ = os.environ.copy()
+        os.environ['WEBKIT_BUGZILLA_USERNAME'] = "foo"
+        os.environ['WEBKIT_BUGZILLA_PASSWORD'] = "bar"
+        username, password = credentials._credentials_from_environment()
+        self.assertEquals(username, "foo")
+        self.assertEquals(password, "bar")
+        os.environ = saved_environ
+
+    def test_read_credentials_without_git_repo(self):
+        # FIXME: This should share more code with test_keyring_without_git_repo
+        class FakeCredentials(MockedCredentials):
+            def _is_mac_os_x(self):
+                return True
+
+            def _credentials_from_keychain(self, username):
+                return ("test@webkit.org", "SECRETSAUCE")
+
+            def _credentials_from_environment(self):
+                return (None, None)
+
+        with _TemporaryDirectory(suffix="not_a_git_repo") as temp_dir_path:
+            credentials = FakeCredentials("bugs.webkit.org", cwd=temp_dir_path)
+            # FIXME: Using read_credentials here seems too broad as higher-priority
+            # credential source could be affected by the user's environment.
+            self.assertEqual(credentials.read_credentials(), ("test@webkit.org", "SECRETSAUCE"))
+
+
+    def test_keyring_without_git_repo(self):
+        # FIXME: This should share more code with test_read_credentials_without_git_repo
+        class MockKeyring(object):
+            def get_password(self, host, username):
+                return "NOMNOMNOM"
+
+        class FakeCredentials(MockedCredentials):
+            def _is_mac_os_x(self):
+                return True
+
+            def _credentials_from_keychain(self, username):
+                return ("test@webkit.org", None)
+
+            def _credentials_from_environment(self):
+                return (None, None)
+
+        with _TemporaryDirectory(suffix="not_a_git_repo") as temp_dir_path:
+            credentials = FakeCredentials("fake.hostname", cwd=temp_dir_path, keyring=MockKeyring())
+            # FIXME: Using read_credentials here seems too broad as higher-priority
+            # credential source could be affected by the user's environment.
+            self.assertEqual(credentials.read_credentials(), ("test@webkit.org", "NOMNOMNOM"))
+
+    def test_keyring_without_git_repo_nor_keychain(self):
+        class MockKeyring(object):
+            def get_password(self, host, username):
+                return "NOMNOMNOM"
+
+        class FakeCredentials(MockedCredentials):
+            def _credentials_from_keychain(self, username):
+                return (None, None)
+
+            def _credentials_from_environment(self):
+                return (None, None)
+
+        class FakeUser(MockUser):
+            @classmethod
+            def prompt(cls, message, repeat=1, raw_input=raw_input):
+                return "test@webkit.org"
+
+            @classmethod
+            def prompt_password(cls, message, repeat=1, raw_input=raw_input):
+                raise AssertionError("should not prompt for password")
+
+        with _TemporaryDirectory(suffix="not_a_git_repo") as temp_dir_path:
+            credentials = FakeCredentials("fake.hostname", cwd=temp_dir_path, keyring=MockKeyring())
+            # FIXME: Using read_credentials here seems too broad as higher-priority
+            # credential source could be affected by the user's environment.
+            self.assertEqual(credentials.read_credentials(FakeUser), ("test@webkit.org", "NOMNOMNOM"))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/common/net/failuremap.py b/Tools/Scripts/webkitpy/common/net/failuremap.py
new file mode 100644
index 0000000..746242e
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/failuremap.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.
+
+
+# FIXME: This probably belongs in the buildbot module.
+class FailureMap(object):
+    def __init__(self):
+        self._failures = []
+
+    def add_regression_window(self, builder, regression_window):
+        self._failures.append({
+            'builder': builder,
+            'regression_window': regression_window,
+        })
+
+    def 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 failing_tests(self):
+        return set(sum([self.tests_failing_for(revision) for revision in self.failing_revisions()], []))
+
+    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/Tools/Scripts/webkitpy/common/net/failuremap_unittest.py b/Tools/Scripts/webkitpy/common/net/failuremap_unittest.py
new file mode 100644
index 0000000..9a66d9e
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/failuremap_unittest.py
@@ -0,0 +1,80 @@
+# 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.common.net.buildbot.buildbot_mock 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'])
+
+    def test_failing_tests(self):
+        failure_map = self._make_failure_map()
+        self.assertEquals(failure_map.failing_tests(), set([u'test1']))
diff --git a/Tools/Scripts/webkitpy/common/net/file_uploader.py b/Tools/Scripts/webkitpy/common/net/file_uploader.py
new file mode 100644
index 0000000..9b220b0
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/file_uploader.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import mimetypes
+import time
+import urllib2
+
+from webkitpy.common.net.networktransaction import NetworkTransaction, NetworkTimeout
+
+
+def get_mime_type(filename):
+    return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
+
+
+# FIXME: Rather than taking tuples, this function should take more structured data.
+def _encode_multipart_form_data(fields, files):
+    """Encode form fields for multipart/form-data.
+
+    Args:
+      fields: A sequence of (name, value) elements for regular form fields.
+      files: A sequence of (name, filename, value) elements for data to be
+             uploaded as files.
+    Returns:
+      (content_type, body) ready for httplib.HTTP instance.
+
+    Source:
+      http://code.google.com/p/rietveld/source/browse/trunk/upload.py
+    """
+    BOUNDARY = '-M-A-G-I-C---B-O-U-N-D-A-R-Y-'
+    CRLF = '\r\n'
+    lines = []
+
+    for key, value in fields:
+        lines.append('--' + BOUNDARY)
+        lines.append('Content-Disposition: form-data; name="%s"' % key)
+        lines.append('')
+        if isinstance(value, unicode):
+            value = value.encode('utf-8')
+        lines.append(value)
+
+    for key, filename, value in files:
+        lines.append('--' + BOUNDARY)
+        lines.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
+        lines.append('Content-Type: %s' % get_mime_type(filename))
+        lines.append('')
+        if isinstance(value, unicode):
+            value = value.encode('utf-8')
+        lines.append(value)
+
+    lines.append('--' + BOUNDARY + '--')
+    lines.append('')
+    body = CRLF.join(lines)
+    content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
+    return content_type, body
+
+
+class FileUploader(object):
+    def __init__(self, url, timeout_seconds):
+        self._url = url
+        self._timeout_seconds = timeout_seconds
+
+    def upload_single_text_file(self, filesystem, content_type, filename):
+        return self._upload_data(content_type, filesystem.read_text_file(filename))
+
+    def upload_as_multipart_form_data(self, filesystem, files, attrs):
+        file_objs = []
+        for filename, path in files:
+            file_objs.append(('file', filename, filesystem.read_binary_file(path)))
+
+        # FIXME: We should use the same variable names for the formal and actual parameters.
+        content_type, data = _encode_multipart_form_data(attrs, file_objs)
+        return self._upload_data(content_type, data)
+
+    def _upload_data(self, content_type, data):
+        def callback():
+            # FIXME: Setting a timeout, either globally using socket.setdefaulttimeout()
+            # or in urlopen(), doesn't appear to work on Mac 10.5 with Python 2.7.
+            # For now we will ignore the timeout value and hope for the best.
+            request = urllib2.Request(self._url, data, {"Content-Type": content_type})
+            return urllib2.urlopen(request)
+
+        return NetworkTransaction(timeout_seconds=self._timeout_seconds).run(callback)
diff --git a/Tools/Scripts/webkitpy/common/net/htdigestparser.py b/Tools/Scripts/webkitpy/common/net/htdigestparser.py
new file mode 100644
index 0000000..ee7d540
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/htdigestparser.py
@@ -0,0 +1,54 @@
+# Copyright (C) 2011 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.
+
+"""htdigestparser - a parser for htdigest files"""
+
+import hashlib
+import string
+
+
+class HTDigestParser(object):
+    def __init__(self, digest_file):
+        self._entries = self.parse_file(digest_file)
+
+    def authenticate(self, username, realm, password):
+        hashed_password = hashlib.md5(':'.join((username, realm, password))).hexdigest()
+        return [username, realm, hashed_password] in self.entries()
+
+    def entries(self):
+        return self._entries
+
+    def parse_file(self, digest_file):
+        entries = [line.rstrip().split(':') for line in digest_file]
+
+        # Perform some sanity-checking to ensure the file is valid.
+        valid_characters = set(string.hexdigits)
+        for entry in entries:
+            if len(entry) != 3:
+                return []
+            hashed_password = entry[-1]
+            if len(hashed_password) != 32:
+                return []
+            if not set(hashed_password).issubset(valid_characters):
+                return []
+
+        return entries
diff --git a/Tools/Scripts/webkitpy/common/net/htdigestparser_unittest.py b/Tools/Scripts/webkitpy/common/net/htdigestparser_unittest.py
new file mode 100644
index 0000000..a2a4ac9
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/htdigestparser_unittest.py
@@ -0,0 +1,82 @@
+# Copyright (C) 2011 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.
+
+import StringIO
+import os
+import unittest
+
+from webkitpy.common.net.htdigestparser import HTDigestParser
+
+
+class HTDigestParserTest(unittest.TestCase):
+    def assertEntriesEqual(self, entries, additional_content=None):
+        digest_file = self.fake_htdigest_file()
+        if additional_content is not None:
+            digest_file.seek(pos=0, mode=os.SEEK_END)
+            digest_file.write(additional_content)
+            digest_file.seek(pos=0, mode=os.SEEK_SET)
+        self.assertEqual(entries, HTDigestParser(digest_file).entries())
+
+    def test_authenticate(self):
+        htdigest = HTDigestParser(self.fake_htdigest_file())
+        self.assertTrue(htdigest.authenticate('user1', 'realm 1', 'password1'))
+        self.assertTrue(htdigest.authenticate('user2', 'realm 2', 'password2'))
+        self.assertTrue(htdigest.authenticate('user3', 'realm 1', 'password3'))
+        self.assertTrue(htdigest.authenticate('user3', 'realm 3', 'password3'))
+
+        self.assertFalse(htdigest.authenticate('user1', 'realm', 'password1'))
+        self.assertFalse(htdigest.authenticate('user1', 'realm 2', 'password1'))
+        self.assertFalse(htdigest.authenticate('user2', 'realm 2', 'password1'))
+        self.assertFalse(htdigest.authenticate('user2', 'realm 1', 'password1'))
+        self.assertFalse(htdigest.authenticate('', '', ''))
+
+    def test_entries(self):
+        entries = [
+            ['user1', 'realm 1', '36b8aa27fa5e9051095d37b619f92762'],
+            ['user2', 'realm 2', '14f827686fa97778f02fe1314a3337c8'],
+            ['user3', 'realm 1', '1817fc8a24119cc57fbafc8a630ea5a5'],
+            ['user3', 'realm 3', 'a05f5a2335e9d87bbe75bbe5e53248f0'],
+        ]
+        self.assertEntriesEqual(entries)
+        self.assertEntriesEqual(entries, additional_content='')
+
+    def test_empty_file(self):
+        self.assertEqual([], HTDigestParser(StringIO.StringIO()).entries())
+
+    def test_too_few_colons(self):
+        self.assertEntriesEqual([], additional_content='user1:realm 1\n')
+
+    def test_too_many_colons(self):
+        self.assertEntriesEqual([], additional_content='user1:realm 1:36b8aa27fa5e9051095d37b619f92762:garbage\n')
+
+    def test_invalid_hash(self):
+        self.assertEntriesEqual([], additional_content='user1:realm 1:36b8aa27fa5e9051095d37b619f92762000000\n')
+        self.assertEntriesEqual([], additional_content='user1:realm 1:36b8aa27fa5e9051095d37b619f9276\n')
+        self.assertEntriesEqual([], additional_content='user1:realm 1:36b8aa27fa5e9051095d37b619f9276z\n')
+        self.assertEntriesEqual([], additional_content='user1:realm 1: 36b8aa27fa5e9051095d37b619f92762\n')
+
+    def fake_htdigest_file(self):
+        return StringIO.StringIO("""user1:realm 1:36b8aa27fa5e9051095d37b619f92762
+user2:realm 2:14f827686fa97778f02fe1314a3337c8
+user3:realm 1:1817fc8a24119cc57fbafc8a630ea5a5
+user3:realm 3:a05f5a2335e9d87bbe75bbe5e53248f0
+""")
diff --git a/Tools/Scripts/webkitpy/common/net/irc/__init__.py b/Tools/Scripts/webkitpy/common/net/irc/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/irc/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/common/net/irc/irc_mock.py b/Tools/Scripts/webkitpy/common/net/irc/irc_mock.py
new file mode 100644
index 0000000..734be06
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/irc/irc_mock.py
@@ -0,0 +1,37 @@
+# Copyright (c) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.system.deprecated_logging import log
+
+
+class MockIRC(object):
+    def post(self, message):
+        log("MOCK: irc.post: %s" % message)
+
+    def disconnect(self):
+        log("MOCK: irc.disconnect")
diff --git a/Tools/Scripts/webkitpy/common/net/irc/ircbot.py b/Tools/Scripts/webkitpy/common/net/irc/ircbot.py
new file mode 100644
index 0000000..c8c1a38
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/irc/ircbot.py
@@ -0,0 +1,108 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.config import irc as config_irc
+
+from webkitpy.common.thread.messagepump import MessagePump, MessagePumpDelegate
+from webkitpy.thirdparty.autoinstalled.irc import ircbot
+from webkitpy.thirdparty.autoinstalled.irc import irclib
+
+
+class IRCBotDelegate(object):
+    def irc_message_received(self, nick, message):
+        raise NotImplementedError, "subclasses must implement"
+
+    def irc_nickname(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def irc_password(self):
+        raise NotImplementedError, "subclasses must implement"
+
+
+class IRCBot(ircbot.SingleServerIRCBot, MessagePumpDelegate):
+    # FIXME: We should get this information from a config file.
+    def __init__(self,
+                 message_queue,
+                 delegate):
+        self._message_queue = message_queue
+        self._delegate = delegate
+        ircbot.SingleServerIRCBot.__init__(
+            self,
+            [(
+                config_irc.server,
+                config_irc.port,
+                self._delegate.irc_password()
+            )],
+            self._delegate.irc_nickname(),
+            self._delegate.irc_nickname())
+        self._channel = config_irc.channel
+
+    # ircbot.SingleServerIRCBot methods
+
+    def on_nicknameinuse(self, connection, event):
+        connection.nick(connection.get_nickname() + "_")
+
+    def on_welcome(self, connection, event):
+        connection.join(self._channel)
+        self._message_pump = MessagePump(self, self._message_queue)
+
+    def on_pubmsg(self, connection, event):
+        nick = irclib.nm_to_n(event.source())
+        request = event.arguments()[0]
+
+        if not irclib.irc_lower(request).startswith(irclib.irc_lower(connection.get_nickname())):
+            return
+
+        if len(request) <= len(connection.get_nickname()):
+            return
+
+        # Some IRC clients, like xchat-gnome, default to using a comma
+        # when addressing someone.
+        vocative_separator = request[len(connection.get_nickname())]
+        if vocative_separator == ':':
+            request = request.split(':', 1)
+        elif vocative_separator == ',':
+            request = request.split(',', 1)
+        else:
+            return
+
+        if len(request) > 1:
+            response = self._delegate.irc_message_received(nick, request[1])
+            if response:
+                connection.privmsg(self._channel, response)
+
+    # MessagePumpDelegate methods
+
+    def schedule(self, interval, callback):
+        self.connection.execute_delayed(interval, callback)
+
+    def message_available(self, message):
+        self.connection.privmsg(self._channel, message)
+
+    def final_message_delivered(self):
+        self.die()
diff --git a/Tools/Scripts/webkitpy/common/net/irc/ircproxy.py b/Tools/Scripts/webkitpy/common/net/irc/ircproxy.py
new file mode 100644
index 0000000..13348b4
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/irc/ircproxy.py
@@ -0,0 +1,62 @@
+# 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 threading
+
+from webkitpy.common.net.irc.ircbot import IRCBot
+from webkitpy.common.thread.threadedmessagequeue import ThreadedMessageQueue
+from webkitpy.common.system.deprecated_logging import log
+
+
+class _IRCThread(threading.Thread):
+    def __init__(self, message_queue, irc_delegate, irc_bot):
+        threading.Thread.__init__(self)
+        self.setDaemon(True)
+        self._message_queue = message_queue
+        self._irc_delegate = irc_delegate
+        self._irc_bot = irc_bot
+
+    def run(self):
+        bot = self._irc_bot(self._message_queue, self._irc_delegate)
+        bot.start()
+
+
+class IRCProxy(object):
+    def __init__(self, irc_delegate, irc_bot=IRCBot):
+        log("Connecting to IRC")
+        self._message_queue = ThreadedMessageQueue()
+        self._child_thread = _IRCThread(self._message_queue, irc_delegate, irc_bot)
+        self._child_thread.start()
+
+    def post(self, message):
+        self._message_queue.post(message)
+
+    def disconnect(self):
+        log("Disconnecting from IRC...")
+        self._message_queue.stop()
+        self._child_thread.join()
diff --git a/Tools/Scripts/webkitpy/common/net/irc/ircproxy_unittest.py b/Tools/Scripts/webkitpy/common/net/irc/ircproxy_unittest.py
new file mode 100644
index 0000000..b44ce40
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/irc/ircproxy_unittest.py
@@ -0,0 +1,43 @@
+# 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.irc.ircproxy import IRCProxy
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.thirdparty.mock import Mock
+
+class IRCProxyTest(unittest.TestCase):
+    def test_trivial(self):
+        def fun():
+            proxy = IRCProxy(Mock(), Mock())
+            proxy.post("hello")
+            proxy.disconnect()
+
+        expected_stderr = "Connecting to IRC\nDisconnecting from IRC...\n"
+        OutputCapture().assert_outputs(self, fun, expected_stderr=expected_stderr)
diff --git a/Tools/Scripts/webkitpy/common/net/layouttestresults.py b/Tools/Scripts/webkitpy/common/net/layouttestresults.py
new file mode 100644
index 0000000..f0d807e
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/layouttestresults.py
@@ -0,0 +1,177 @@
+# 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
+# This class is one big hack and only needs to exist until we transition to new-run-webkit-tests.
+
+from webkitpy.common.net.resultsjsonparser import ResultsJSONParser
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup, SoupStrainer
+from webkitpy.layout_tests.models import test_results
+from webkitpy.layout_tests.models import test_failures
+
+
+# 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
+
+
+class ORWTResultsHTMLParser(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:'
+    # FIXME: This may need to be made aware of WebKitTestRunner results for WebKit2.
+    crash_key = u'Tests that caused the DumpRenderTree tool to crash:'
+    missing_key = u'Tests that had no expected results (probably new):'
+    webprocess_crash_key = u'Tests that caused the Web process to crash:'
+
+    expected_keys = [
+        stderr_key,
+        fail_key,
+        crash_key,
+        webprocess_crash_key,
+        timeout_key,
+        missing_key,
+    ]
+
+    @classmethod
+    def _failures_from_fail_row(self, row):
+        # Look at all anchors in this row, and guess what type
+        # of new-run-webkit-test failures they equate to.
+        failures = set()
+        test_name = None
+        for anchor in row.findAll("a"):
+            anchor_text = unicode(anchor.string)
+            if not test_name:
+                test_name = anchor_text
+                continue
+            if anchor_text in ["expected image", "image diffs"] or '%' in anchor_text:
+                failures.add(test_failures.FailureImageHashMismatch())
+            elif anchor_text in ["expected", "actual", "diff", "pretty diff"]:
+                failures.add(test_failures.FailureTextMismatch())
+            else:
+                log("Unhandled link text in results.html parsing: %s.  Please file a bug against webkitpy." % anchor_text)
+        # FIXME: Its possible the row contained no links due to ORWT brokeness.
+        # We should probably assume some type of failure anyway.
+        return failures
+
+    @classmethod
+    def _failures_from_row(cls, row, table_title):
+        if table_title == cls.fail_key:
+            return cls._failures_from_fail_row(row)
+        if table_title == cls.crash_key:
+            return [test_failures.FailureCrash()]
+        if table_title == cls.webprocess_crash_key:
+            return [test_failures.FailureCrash(process_name="WebProcess")]
+        if table_title == cls.timeout_key:
+            return [test_failures.FailureTimeout()]
+        if table_title == cls.missing_key:
+            return [test_failures.FailureMissingResult(), test_failures.FailureMissingImageHash(), test_failures.FailureMissingImage()]
+        return None
+
+    @classmethod
+    def _test_result_from_row(cls, row, table_title):
+        test_name = unicode(row.find("a").string)
+        failures = cls._failures_from_row(row, table_title)
+        # TestResult is a class designed to work with new-run-webkit-tests.
+        # old-run-webkit-tests does not save quite enough information in results.html for us to parse.
+        # FIXME: It's unclear if test_name should include LayoutTests or not.
+        return test_results.TestResult(test_name, failures)
+
+    @classmethod
+    def _parse_results_table(cls, table):
+        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)
+        # Ignore stderr failures.  Everyone ignores them anyway.
+        if table_title == cls.stderr_key:
+            return []
+        # FIXME: We might end with two TestResults object for the same test if it appears in more than one row.
+        return [cls._test_result_from_row(row, table_title) for row in table.findAll("tr")]
+
+    @classmethod
+    def parse_results_html(cls, page):
+        tables = BeautifulSoup(page).findAll("table")
+        return sum([cls._parse_results_table(table) for table in tables], [])
+
+
+# FIXME: This should be unified with ResultsSummary or other NRWT layout tests 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):
+    @classmethod
+    def results_from_string(cls, string):
+        if not string:
+            return None
+        # For now we try to parse first as json, then as results.html
+        # eventually we will remove the html fallback support.
+        test_results = ResultsJSONParser.parse_results_json(string)
+        if not test_results:
+            test_results = ORWTResultsHTMLParser.parse_results_html(string)
+        if not test_results:
+            return None
+        return cls(test_results)
+
+    def __init__(self, test_results):
+        self._test_results = test_results
+        self._failure_limit_count = None
+        self._unit_test_failures = []
+
+    # FIXME: run-webkit-tests should store the --exit-after-N-failures value
+    # (or some indication of early exit) somewhere in the results.html/results.json
+    # file.  Until it does, callers should set the limit to
+    # --exit-after-N-failures value used in that run.  Consumers of LayoutTestResults
+    # may use that value to know if absence from the failure list means PASS.
+    # https://bugs.webkit.org/show_bug.cgi?id=58481
+    def set_failure_limit_count(self, limit):
+        self._failure_limit_count = limit
+
+    def failure_limit_count(self):
+        return self._failure_limit_count
+
+    def test_results(self):
+        return self._test_results
+
+    def results_matching_failure_types(self, failure_types):
+        return [result for result in self._test_results if result.has_failure_matching_types(*failure_types)]
+
+    def tests_matching_failure_types(self, failure_types):
+        return [result.test_name for result in self.results_matching_failure_types(failure_types)]
+
+    def failing_test_results(self):
+        return self.results_matching_failure_types(test_failures.ALL_FAILURE_CLASSES)
+
+    def failing_tests(self):
+        return [result.test_name for result in self.failing_test_results()] + self._unit_test_failures
+
+    def add_unit_test_failures(self, unit_test_results):
+        self._unit_test_failures = unit_test_results
diff --git a/Tools/Scripts/webkitpy/common/net/layouttestresults_unittest.py b/Tools/Scripts/webkitpy/common/net/layouttestresults_unittest.py
new file mode 100644
index 0000000..939a56a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/layouttestresults_unittest.py
@@ -0,0 +1,146 @@
+# 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, ORWTResultsHTMLParser
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.layout_tests.models import test_results
+from webkitpy.layout_tests.models import test_failures
+from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup
+
+
+class ORWTResultsHTMLParserTest(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>
+"""
+
+    _example_results_html_with_failing_tests = """
+<html>
+<head>
+<title>Layout Test Results</title>
+</head>
+<body>
+<p>Tests where results did not match expected results:</p>
+<table>
+<tr>
+<td><a href="http://trac.webkit.org/export/91245/trunk/LayoutTests/compositing/plugins/composited-plugin.html">compositing/plugins/composited-plugin.html</a></td>
+<td>
+<a href="compositing/plugins/composited-plugin-expected.txt">expected</a>
+</td>
+<td>
+<a href="compositing/plugins/composited-plugin-actual.txt">actual</a>
+</td>
+<td>
+<a href="compositing/plugins/composited-plugin-diffs.txt">diff</a>
+</td>
+<td>
+<a href="compositing/plugins/composited-plugin-pretty-diff.html">pretty diff</a>
+</td>
+</tr>
+</table>
+<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>
+"""
+
+    def test_parse_layout_test_results(self):
+        failures = [test_failures.FailureMissingResult(), test_failures.FailureMissingImageHash(), test_failures.FailureMissingImage()]
+        testname = 'fast/repaint/no-caret-repaint-in-non-content-editable-element.html'
+        expected_results = [test_results.TestResult(testname, failures)]
+
+        results = ORWTResultsHTMLParser.parse_results_html(self._example_results_html)
+        self.assertEqual(expected_results, results)
+
+
+    def test_failures_from_fail_row(self):
+        row = BeautifulSoup("<tr><td><a>test.hml</a></td><td><a>expected image</a></td><td><a>25%</a></td></tr>")
+        test_name = unicode(row.find("a").string)
+        # Even if the caller has already found the test name, findAll inside _failures_from_fail_row will see it again.
+        failures = OutputCapture().assert_outputs(self, ORWTResultsHTMLParser._failures_from_fail_row, [row])
+        self.assertEqual(len(failures), 1)
+        self.assertEqual(type(sorted(failures)[0]), test_failures.FailureImageHashMismatch)
+
+        row = BeautifulSoup("<tr><td><a>test.hml</a><a>foo</a></td></tr>")
+        expected_stderr = "Unhandled link text in results.html parsing: foo.  Please file a bug against webkitpy.\n"
+        OutputCapture().assert_outputs(self, ORWTResultsHTMLParser._failures_from_fail_row, [row], expected_stderr=expected_stderr)
+
+
+class LayoutTestResultsTest(unittest.TestCase):
+
+    def test_set_failure_limit_count(self):
+        results = LayoutTestResults([])
+        self.assertEquals(results.failure_limit_count(), None)
+        results.set_failure_limit_count(10)
+        self.assertEquals(results.failure_limit_count(), 10)
+
+    def test_results_from_string(self):
+        self.assertEqual(LayoutTestResults.results_from_string(None), None)
+        self.assertEqual(LayoutTestResults.results_from_string(""), None)
+        results = LayoutTestResults.results_from_string(ORWTResultsHTMLParserTest._example_results_html)
+        self.assertEqual(len(results.failing_tests()), 1)
+
+    def test_tests_matching_failure_types(self):
+        results = LayoutTestResults.results_from_string(ORWTResultsHTMLParserTest._example_results_html_with_failing_tests)
+        failing_tests = results.tests_matching_failure_types([test_failures.FailureTextMismatch])
+        self.assertEqual(len(results.failing_tests()), 2)
diff --git a/Tools/Scripts/webkitpy/common/net/networktransaction.py b/Tools/Scripts/webkitpy/common/net/networktransaction.py
new file mode 100644
index 0000000..03b1432
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/networktransaction.py
@@ -0,0 +1,71 @@
+# 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 logging
+import time
+import urllib2
+
+from webkitpy.common.system.deprecated_logging import log
+
+
+_log = logging.getLogger(__name__)
+
+
+class NetworkTimeout(Exception):
+    def __str__(self):
+        return 'NetworkTimeout'
+
+
+class NetworkTransaction(object):
+    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
+        self._backoff_seconds = self._initial_backoff_seconds
+        while True:
+            try:
+                return request()
+            except urllib2.HTTPError, e:
+                if self._convert_404_to_None and e.code == 404:
+                    return None
+                self._check_for_timeout()
+                _log.warn("Received HTTP status %s loading \"%s\".  Retrying in %s seconds..." % (e.code, e.filename, self._backoff_seconds))
+                self._sleep()
+
+    def _check_for_timeout(self):
+        if self._total_sleep + self._backoff_seconds > self._timeout_seconds:
+            raise NetworkTimeout()
+
+    def _sleep(self):
+        time.sleep(self._backoff_seconds)
+        self._total_sleep += self._backoff_seconds
+        self._backoff_seconds *= self._grown_factor
diff --git a/Tools/Scripts/webkitpy/common/net/networktransaction_unittest.py b/Tools/Scripts/webkitpy/common/net/networktransaction_unittest.py
new file mode 100644
index 0000000..3302dec
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/networktransaction_unittest.py
@@ -0,0 +1,94 @@
+# 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.networktransaction import NetworkTransaction, NetworkTimeout
+from webkitpy.common.system.logtesting import LoggingTestCase
+
+
+class NetworkTransactionTest(LoggingTestCase):
+    exception = Exception("Test exception")
+
+    def test_success(self):
+        transaction = NetworkTransaction()
+        self.assertEqual(transaction.run(lambda: 42), 42)
+
+    def _raise_exception(self):
+        raise self.exception
+
+    def test_exception(self):
+        transaction = NetworkTransaction()
+        did_process_exception = False
+        did_throw_exception = True
+        try:
+            transaction.run(lambda: self._raise_exception())
+            did_throw_exception = False
+        except Exception, e:
+            did_process_exception = True
+            self.assertEqual(e, self.exception)
+        self.assertTrue(did_throw_exception)
+        self.assertTrue(did_process_exception)
+
+    def _raise_500_error(self):
+        self._run_count += 1
+        if self._run_count < 3:
+            from webkitpy.thirdparty.autoinstalled.mechanize import HTTPError
+            raise HTTPError("http://example.com/", 500, "internal server error", None, None)
+        return 42
+
+    def _raise_404_error(self):
+        from webkitpy.thirdparty.autoinstalled.mechanize import HTTPError
+        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_500_error()), 42)
+        self.assertEqual(self._run_count, 3)
+        self.assertLog(['WARNING: Received HTTP status 500 loading "http://example.com/".  '
+                        'Retrying in 0 seconds...\n',
+                        'WARNING: Received HTTP status 500 loading "http://example.com/".  '
+                        '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_500_error())
+            did_throw_exception = False
+        except NetworkTimeout, e:
+            did_process_exception = True
+        self.assertTrue(did_throw_exception)
+        self.assertTrue(did_process_exception)
diff --git a/Tools/Scripts/webkitpy/common/net/omahaproxy.py b/Tools/Scripts/webkitpy/common/net/omahaproxy.py
new file mode 100644
index 0000000..b7b481f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/omahaproxy.py
@@ -0,0 +1,81 @@
+# Copyright (C) 2012 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# This is the client to query http://omahaproxy.appspot.com/ to retrieve
+# chrome versions associated with WebKit commits.
+
+from webkitpy.common.net.networktransaction import NetworkTransaction
+from webkitpy.common.config import urls
+
+import json
+import urllib2
+
+
+class OmahaProxy(object):
+    default_url = urls.omahaproxy_url
+
+    chrome_platforms = {"linux": "Linux",
+                        "win": "Windows",
+                        "mac": "Mac",
+                        "cros": "Chrome OS",
+                        "cf": "Chrome Frame",
+                        "ios": "iOS"}
+    chrome_channels = ["canary", "dev", "beta", "stable"]
+
+    def __init__(self, url=default_url, browser=None):
+        self._chrome_channels = set(self.chrome_channels)
+        self.set_url(url)
+        from webkitpy.thirdparty.autoinstalled.mechanize import Browser
+        self._browser = browser or Browser()
+
+    def set_url(self, url):
+        self.url = url
+
+    def _json_url(self):
+        return "%s/all.json" % self.url
+
+    def _get_json(self):
+        return NetworkTransaction().run(lambda: urllib2.urlopen(self._json_url()).read())
+
+    def get_revisions(self):
+        revisions_json = json.loads(self._get_json())
+        revisions = []
+        for platform in revisions_json:
+            for version in platform["versions"]:
+                try:
+                    row = {
+                        "commit": int(version["base_webkit_revision"]),
+                        "channel": version["channel"],
+                        "platform": self.chrome_platforms.get(platform["os"], platform["os"]),
+                        "date": version["date"],
+                    }
+                    assert(version["channel"] in self._chrome_channels)
+                    revisions.append(row)
+                except ValueError:
+                    next
+        return revisions
diff --git a/Tools/Scripts/webkitpy/common/net/omahaproxy_unittest.py b/Tools/Scripts/webkitpy/common/net/omahaproxy_unittest.py
new file mode 100644
index 0000000..f3e5be3
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/omahaproxy_unittest.py
@@ -0,0 +1,139 @@
+# Copyright (C) 2012 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.
+#
+
+# Unit test for omahaproxy.py
+
+import unittest
+
+from webkitpy.common.net.omahaproxy import OmahaProxy
+
+
+class MockOmahaProxy(OmahaProxy):
+    def __init__(self, json):
+        self._get_json = lambda: json
+        OmahaProxy.__init__(self)
+
+
+class OmahaProxyTest(unittest.TestCase):
+    example_omahaproxy_json = """[
+        {"os": "win",
+         "versions": [
+                {"base_webkit_revision": "116185",
+                 "v8_ver": "3.10.8.1",
+                 "wk_ver": "536.11",
+                 "base_trunk_revision": 135598,
+                 "prev_version": "20.0.1128.0",
+                 "version": "20.0.1129.0",
+                 "date": "05\/07\/12",
+                 "prev_date": "05\/06\/12",
+                 "true_branch": "trunk",
+                 "channel": "canary",
+                 "branch_revision": "NA"},
+                {"base_webkit_revision": "115687",
+                 "v8_ver": "3.10.6.0",
+                 "wk_ver": "536.10",
+                 "base_trunk_revision": 134666,
+                 "prev_version": "20.0.1123.1",
+                 "version": "20.0.1123.4",
+                 "date": "05\/04\/12",
+                 "prev_date": "05\/02\/12",
+                 "true_branch": "1123",
+                 "channel": "dev",
+                 "branch_revision": 135092}]},
+        {"os": "linux",
+         "versions": [
+                {"base_webkit_revision": "115688",
+                 "v8_ver": "3.10.6.0",
+                 "wk_ver": "536.10",
+                 "base_trunk_revision": 134666,
+                 "prev_version": "20.0.1123.2",
+                 "version": "20.0.1123.4",
+                 "date": "05\/04\/12",
+                 "prev_date": "05\/02\/12",
+                 "true_branch": "1123",
+                 "channel": "dev",
+                 "branch_revision": 135092},
+                {"base_webkit_revision": "112327",
+                 "v8_ver": "3.9.24.17",
+                 "wk_ver": "536.5",
+                 "base_trunk_revision": 129376,
+                 "prev_version": "19.0.1084.36",
+                 "version": "19.0.1084.41",
+                 "date": "05\/03\/12",
+                 "prev_date": "04\/25\/12",
+                 "true_branch": "1084",
+                 "channel": "beta",
+                 "branch_revision": 134854},
+                {"base_webkit_revision": "*",
+                 "v8_ver": "3.9.24.17",
+                 "wk_ver": "536.5",
+                 "base_trunk_revision": 129376,
+                 "prev_version": "19.0.1084.36",
+                 "version": "19.0.1084.41",
+                 "date": "05\/03\/12",
+                 "prev_date": "04\/25\/12",
+                 "true_branch": "1084",
+                 "channel": "release",
+                 "branch_revision": 134854}]},
+        {"os": "weird-platform",
+         "versions": [
+                {"base_webkit_revision": "115688",
+                 "v8_ver": "3.10.6.0",
+                 "wk_ver": "536.10",
+                 "base_trunk_revision": 134666,
+                 "prev_version": "20.0.1123.2",
+                 "version": "20.0.1123.4",
+                 "date": "05\/04\/12",
+                 "prev_date": "05\/02\/12",
+                 "true_branch": "1123",
+                 "channel": "dev",
+                 "branch_revision": 135092}]}]"""
+
+    expected_revisions = [
+        {"commit": 116185, "channel": "canary", "platform": "Windows", "date": "05/07/12"},
+        {"commit": 115687, "channel": "dev", "platform": "Windows", "date": "05/04/12"},
+        {"commit": 115688, "channel": "dev", "platform": "Linux", "date": "05/04/12"},
+        {"commit": 112327, "channel": "beta", "platform": "Linux", "date": "05/03/12"},
+        {"commit": 115688, "channel": "dev", "platform": "weird-platform", "date": "05/04/12"},
+    ]
+
+    def test_get_revisions(self):
+        omahaproxy = MockOmahaProxy(self.example_omahaproxy_json)
+        revisions = omahaproxy.get_revisions()
+        self.assertEqual(len(revisions), 5)
+        for revision in revisions:
+            self.assertTrue("commit" in revision)
+            self.assertTrue("channel" in revision)
+            self.assertTrue("platform" in revision)
+            self.assertTrue("date" in revision)
+            self.assertEqual(len(revision.keys()), 4)
+        self.assertEqual(revisions, self.expected_revisions)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/common/net/regressionwindow.py b/Tools/Scripts/webkitpy/common/net/regressionwindow.py
new file mode 100644
index 0000000..3960ba2
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/regressionwindow.py
@@ -0,0 +1,52 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+# FIXME: This probably belongs in the buildbot module.
+class RegressionWindow(object):
+    def __init__(self, build_before_failure, failing_build, failing_tests=None):
+        self._build_before_failure = build_before_failure
+        self._failing_build = failing_build
+        self._failing_tests = failing_tests
+        self._revisions = None
+
+    def build_before_failure(self):
+        return self._build_before_failure
+
+    def failing_build(self):
+        return self._failing_build
+
+    def failing_tests(self):
+        return self._failing_tests
+
+    def revisions(self):
+        # 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/Tools/Scripts/webkitpy/common/net/resultsjsonparser.py b/Tools/Scripts/webkitpy/common/net/resultsjsonparser.py
new file mode 100644
index 0000000..42ce56a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/resultsjsonparser.py
@@ -0,0 +1,154 @@
+# 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 json
+
+from webkitpy.common.memoized import memoized
+from webkitpy.common.system.deprecated_logging import log
+# FIXME: common should never import from new-run-webkit-tests, one of these files needs to move.
+from webkitpy.layout_tests.layout_package import json_results_generator
+from webkitpy.layout_tests.models import test_expectations, test_results, test_failures
+from webkitpy.layout_tests.models.test_expectations import TestExpectations
+
+
+# These are helper functions for navigating the results json structure.
+def for_each_test(tree, handler, prefix=''):
+    for key in tree:
+        new_prefix = (prefix + '/' + key) if prefix else key
+        if 'actual' not in tree[key]:
+            for_each_test(tree[key], handler, new_prefix)
+        else:
+            handler(new_prefix, tree[key])
+
+
+def result_for_test(tree, test):
+    parts = test.split('/')
+    for part in parts:
+        tree = tree[part]
+    return tree
+
+
+# Wrapper around the dictionaries returned from the json.
+# Eventually the .json should just serialize the TestFailure objects
+# directly and we won't need this.
+class JSONTestResult(object):
+    def __init__(self, test_name, result_dict):
+        self._test_name = test_name
+        self._result_dict = result_dict
+
+    def did_pass_or_run_as_expected(self):
+        return self.did_pass() or self.did_run_as_expected()
+
+    def did_pass(self):
+        return test_expectations.PASS in self._actual_as_tokens()
+
+    def did_run_as_expected(self):
+        actual_results = self._actual_as_tokens()
+        expected_results = self._expected_as_tokens()
+        # FIXME: We should only call remove_pixel_failures when this JSONResult
+        # came from a test run without pixel tests!
+        if not TestExpectations.has_pixel_failures(actual_results):
+            expected_results = TestExpectations.remove_pixel_failures(expected_results)
+        for actual_result in actual_results:
+            if not TestExpectations.result_was_expected(actual_result, expected_results, False, False):
+                return False
+        return True
+
+    def _tokenize(self, results_string):
+        tokens = map(TestExpectations.expectation_from_string, results_string.split(' '))
+        if None in tokens:
+            log("Unrecognized result in %s" % results_string)
+        return set(tokens)
+
+    @memoized
+    def _actual_as_tokens(self):
+        actual_results = self._result_dict['actual']
+        return self._tokenize(actual_results)
+
+    @memoized
+    def _expected_as_tokens(self):
+        actual_results = self._result_dict['expected']
+        return self._tokenize(actual_results)
+
+    def _failure_types_from_actual_result(self, actual):
+        # FIXME: There doesn't seem to be a full list of all possible values of
+        # 'actual' anywhere.  However JSONLayoutResultsGenerator.FAILURE_TO_CHAR
+        # is a useful reference as that's for "old" style results.json files
+        #
+        # FIXME: TEXT, IMAGE_PLUS_TEXT, and AUDIO are obsolete but we keep them for
+        # now so that we can parse old results.json files.
+        if actual == test_expectations.PASS:
+            return []
+        elif actual == test_expectations.FAIL:
+            return [test_failures.FailureTextMismatch(), test_failures.FailureImageHashMismatch(), test_failures.FailureAudioMismatch()]
+        elif actual == test_expectations.TEXT:
+            return [test_failures.FailureTextMismatch()]
+        elif actual == test_expectations.IMAGE:
+            return [test_failures.FailureImageHashMismatch()]
+        elif actual == test_expectations.IMAGE_PLUS_TEXT:
+            return [test_failures.FailureImageHashMismatch(), test_failures.FailureTextMismatch()]
+        elif actual == test_expectations.AUDIO:
+            return [test_failures.FailureAudioMismatch()]
+        elif actual == test_expectations.TIMEOUT:
+            return [test_failures.FailureTimeout()]
+        elif actual == test_expectations.CRASH:
+            # NOTE: We don't know what process crashed from the json, just that a process crashed.
+            return [test_failures.FailureCrash()]
+        elif actual == test_expectations.MISSING:
+            return [test_failures.FailureMissingResult(), test_failures.FailureMissingImageHash(), test_failures.FailureMissingImage()]
+        else:
+            log("Failed to handle: %s" % self._result_dict['actual'])
+            return []
+
+    def _failures(self):
+        if self.did_pass():
+            return []
+        return sum(map(self._failure_types_from_actual_result, self._actual_as_tokens()), [])
+
+    def test_result(self):
+        # FIXME: Optionally pull in the test runtime from times_ms.json.
+        return test_results.TestResult(self._test_name, self._failures())
+
+
+class ResultsJSONParser(object):
+    @classmethod
+    def parse_results_json(cls, json_string):
+        if not json_results_generator.has_json_wrapper(json_string):
+            return None
+
+        content_string = json_results_generator.strip_json_wrapper(json_string)
+        json_dict = json.loads(content_string)
+
+        json_results = []
+        for_each_test(json_dict['tests'], lambda test, result: json_results.append(JSONTestResult(test, result)))
+
+        # FIXME: What's the short sexy python way to filter None?
+        # I would use [foo.bar() for foo in foos if foo.bar()] but bar() is expensive.
+        unexpected_failures = [result.test_result() for result in json_results if not result.did_pass_or_run_as_expected()]
+        return filter(lambda a: a, unexpected_failures)
diff --git a/Tools/Scripts/webkitpy/common/net/resultsjsonparser_unittest.py b/Tools/Scripts/webkitpy/common/net/resultsjsonparser_unittest.py
new file mode 100644
index 0000000..867379f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/resultsjsonparser_unittest.py
@@ -0,0 +1,93 @@
+# 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.resultsjsonparser import ResultsJSONParser
+from webkitpy.layout_tests.models import test_results
+from webkitpy.layout_tests.models import test_failures
+
+
+class ResultsJSONParserTest(unittest.TestCase):
+    # The real files have no whitespace, but newlines make this much more readable.
+
+    _example_full_results_json = """ADD_RESULTS({
+    "tests": {
+        "fast": {
+            "dom": {
+                "prototype-inheritance.html": {
+                    "expected": "PASS",
+                    "actual": "FAIL"
+                },
+                "prototype-banana.html": {
+                    "expected": "FAIL",
+                    "actual": "PASS"
+                },
+                "prototype-taco.html": {
+                    "expected": "PASS",
+                    "actual": "PASS FAIL"
+                },
+                "prototype-chocolate.html": {
+                    "expected": "FAIL",
+                    "actual": "FAIL"
+                },
+                "prototype-strawberry.html": {
+                    "expected": "PASS",
+                    "actual": "FAIL PASS"
+                }
+            }
+        },
+        "svg": {
+            "dynamic-updates": {
+                "SVGFEDropShadowElement-dom-stdDeviation-attr.html": {
+                    "expected": "PASS",
+                    "actual": "IMAGE",
+                    "has_stderr": true
+                }
+            }
+        }
+    },
+    "skipped": 450,
+    "num_regressions": 15,
+    "layout_tests_dir": "\/b\/build\/slave\/Webkit_Mac10_5\/build\/src\/third_party\/WebKit\/LayoutTests",
+    "version": 3,
+    "num_passes": 77,
+    "has_pretty_patch": false,
+    "fixable": 1220,
+    "num_flaky": 0,
+    "uses_expectations_file": true,
+    "has_wdiff": false
+});"""
+
+    def test_basic(self):
+        expected_results = [
+            test_results.TestResult("svg/dynamic-updates/SVGFEDropShadowElement-dom-stdDeviation-attr.html", [test_failures.FailureImageHashMismatch()], 0),
+            test_results.TestResult("fast/dom/prototype-inheritance.html", [test_failures.FailureTextMismatch(), test_failures.FailureImageHashMismatch(), test_failures.FailureAudioMismatch()], 0),
+        ]
+        results = ResultsJSONParser.parse_results_json(self._example_full_results_json)
+        self.assertEqual(expected_results, results)
diff --git a/Tools/Scripts/webkitpy/common/net/statusserver.py b/Tools/Scripts/webkitpy/common/net/statusserver.py
new file mode 100644
index 0000000..2bda1ce
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/statusserver.py
@@ -0,0 +1,170 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# This the client designed to talk to Tools/QueueStatusServer.
+
+from webkitpy.common.net.networktransaction import NetworkTransaction
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup
+
+import logging
+import urllib2
+
+
+_log = logging.getLogger(__name__)
+
+
+class StatusServer:
+    # FIXME: This should probably move to common.config.urls.
+    default_host = "queues.webkit.org"
+
+    def __init__(self, host=default_host, browser=None, bot_id=None):
+        self.set_host(host)
+        from webkitpy.thirdparty.autoinstalled.mechanize import 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)
+
+    def _add_patch(self, patch):
+        if not patch:
+            return
+        if patch.bug_id():
+            self._browser["bug_id"] = unicode(patch.bug_id())
+        if 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')
+
+    # 500 is the AppEngine limit for TEXT fields (which most of our fields are).
+    # Exceeding the limit will result in a 500 error from the server.
+    def _set_field(self, field_name, value, limit=500):
+        if len(value) > limit:
+            _log.warn("Attempted to set %s to value exceeding %s characters, truncating." % (field_name, limit))
+        self._browser[field_name] = value[:limit]
+
+    def _post_status_to_server(self, queue_name, status, patch, results_file):
+        if results_file:
+            # We might need to re-wind the file if we've already tried to post it.
+            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
+        if self.bot_id:
+            self._browser["bot_id"] = self.bot_id
+        self._add_patch(patch)
+        self._set_field("status", status, limit=500)
+        self._add_results_file(results_file)
+        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()
+
+    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
+        work_items = map(unicode, work_items)  # .join expects strings
+        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)
+        next_patch_url = "%s/next-patch/%s" % (self.url, queue_name)
+        return self._fetch_url(next_patch_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.info("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))
+
+    def update_status(self, queue_name, status, patch=None, results_file=None):
+        log(status)
+        return NetworkTransaction().run(lambda: self._post_status_to_server(queue_name, status, patch, results_file))
+
+    def update_svn_revision(self, svn_revision_number, broken_bot):
+        log("SVN revision: %s broke %s" % (svn_revision_number, broken_bot))
+        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:
+            if e.code == 404:
+                return None
+            raise e
+
+    def patch_status(self, queue_name, patch_id):
+        patch_status_url = "%s/patch-status/%s/%s" % (self.url, queue_name, patch_id)
+        return self._fetch_url(patch_status_url)
+
+    def svn_revision(self, svn_revision_number):
+        svn_revision_url = "%s/svn-revision/%s" % (self.url, svn_revision_number)
+        return self._fetch_url(svn_revision_url)
diff --git a/Tools/Scripts/webkitpy/common/net/statusserver_mock.py b/Tools/Scripts/webkitpy/common/net/statusserver_mock.py
new file mode 100644
index 0000000..69d1ae8
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/statusserver_mock.py
@@ -0,0 +1,68 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.system.deprecated_logging import log
+
+
+class MockStatusServer(object):
+
+    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):
+        return None
+
+    def svn_revision(self, svn_revision):
+        return None
+
+    def next_work_item(self, queue_name):
+        if not self._work_items:
+            return None
+        return self._work_items.pop(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
+
+    def update_svn_revision(self, svn_revision, broken_bot):
+        return 191
+
+    def results_url_for_status(self, status_id):
+        return "http://dummy_url"
diff --git a/Tools/Scripts/webkitpy/common/net/statusserver_unittest.py b/Tools/Scripts/webkitpy/common/net/statusserver_unittest.py
new file mode 100644
index 0000000..1f0afd0
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/statusserver_unittest.py
@@ -0,0 +1,43 @@
+# 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.statusserver import StatusServer
+from webkitpy.common.system.outputcapture import OutputCaptureTestCaseBase
+from webkitpy.common.net.web_mock import MockBrowser
+
+
+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/Tools/Scripts/webkitpy/common/net/unittestresults.py b/Tools/Scripts/webkitpy/common/net/unittestresults.py
new file mode 100644
index 0000000..bb82b05
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/unittestresults.py
@@ -0,0 +1,50 @@
+# Copyright (c) 2012, 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 xml.dom.minidom
+
+from webkitpy.common.system.deprecated_logging import log
+
+
+class UnitTestResults(object):
+    @classmethod
+    def results_from_string(self, string):
+        if not string:
+            return None
+        try:
+            dom = xml.dom.minidom.parseString(string)
+            failures = []
+            for testcase in dom.getElementsByTagName('testcase'):
+                if testcase.getElementsByTagName('failure').length != 0:
+                    testname = testcase.getAttribute('name')
+                    classname = testcase.getAttribute('classname')
+                    failures.append("%s.%s" % (classname, testname))
+            return failures
+        except xml.parsers.expat.ExpatError, e:
+            log("XML error %s parsing unit test output" % str(e))
+            return None
diff --git a/Tools/Scripts/webkitpy/common/net/unittestresults_unittest.py b/Tools/Scripts/webkitpy/common/net/unittestresults_unittest.py
new file mode 100644
index 0000000..f885206
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/unittestresults_unittest.py
@@ -0,0 +1,98 @@
+# Copyright (c) 2012, 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 unittestresults import UnitTestResults
+
+
+class UnitTestResultsTest(unittest.TestCase):
+
+    def test_nostring(self):
+        self.assertEquals(None, UnitTestResults.results_from_string(None))
+
+    def test_emptystring(self):
+        self.assertEquals(None, UnitTestResults.results_from_string(""))
+
+    def test_nofailures(self):
+        no_failures_xml = """<?xml version="1.0" encoding="UTF-8"?>
+<testsuites tests="3" failures="0" disabled="0" errors="0" time="11.35" name="AllTests">
+  <testsuite name="RenderTableCellDeathTest" tests="3" failures="0" disabled="0" errors="0" time="0.677">
+    <testcase name="CanSetColumn" status="run" time="0.168" classname="RenderTableCellDeathTest" />
+    <testcase name="CrashIfSettingUnsetColumnIndex" status="run" time="0.129" classname="RenderTableCellDeathTest" />
+    <testcase name="CrashIfSettingUnsetRowIndex" status="run" time="0.123" classname="RenderTableCellDeathTest" />
+  </testsuite>
+</testsuites>"""
+        self.assertEquals([], UnitTestResults.results_from_string(no_failures_xml))
+
+    def test_onefailure(self):
+        one_failure_xml = """<?xml version="1.0" encoding="UTF-8"?>
+<testsuites tests="4" failures="1" disabled="0" errors="0" time="11.35" name="AllTests">
+  <testsuite name="RenderTableCellDeathTest" tests="4" failures="1" disabled="0" errors="0" time="0.677">
+    <testcase name="CanSetColumn" status="run" time="0.168" classname="RenderTableCellDeathTest" />
+    <testcase name="CrashIfSettingUnsetColumnIndex" status="run" time="0.129" classname="RenderTableCellDeathTest" />
+    <testcase name="CrashIfSettingUnsetRowIndex" status="run" time="0.123" classname="RenderTableCellDeathTest" />
+    <testcase name="FAILS_DivAutoZoomParamsTest" status="run" time="0.02" classname="WebFrameTest">
+      <failure message="Value of: scale&#x0A;  Actual: 4&#x0A;Expected: 1" type=""><![CDATA[../../Source/WebKit/chromium/tests/WebFrameTest.cpp:191
+Value of: scale
+  Actual: 4
+Expected: 1]]></failure>
+    </testcase>
+  </testsuite>
+</testsuites>"""
+        expected = ["WebFrameTest.FAILS_DivAutoZoomParamsTest"]
+        self.assertEquals(expected, UnitTestResults.results_from_string(one_failure_xml))
+
+    def test_multiple_failures_per_test(self):
+        multiple_failures_per_test_xml = """<?xml version="1.0" encoding="UTF-8"?>
+<testsuites tests="4" failures="2" disabled="0" errors="0" time="11.35" name="AllTests">
+  <testsuite name="UnitTests" tests="4" failures="2" disable="0" errors="0" time="10.0">
+    <testcase name="TestOne" status="run" time="0.5" classname="ClassOne">
+      <failure message="Value of: pi&#x0A;  Actual: 3&#x0A;Expected: 3.14" type=""><![CDATA[../../Source/WebKit/chromium/tests/ClassOneTest.cpp:42
+Value of: pi
+  Actual: 3
+Expected: 3.14]]></failure>
+    </testcase>
+    <testcase name="TestTwo" status="run" time="0.5" classname="ClassTwo">
+      <failure message="Value of: e&#x0A;  Actual: 2&#x0A;Expected: 2.71" type=""><![CDATA[../../Source/WebKit/chromium/tests/ClassTwoTest.cpp:30
+Value of: e
+  Actual: 2
+Expected: 2.71]]></failure>
+      <failure message="Value of: tau&#x0A;  Actual: 6&#x0A;Expected: 6.28" type=""><![CDATA[../../Source/WebKit/chromium/tests/ClassTwoTest.cpp:55
+Value of: tau
+  Actual: 6
+Expected: 6.28]]></failure>
+    </testcase>
+  </testsuite>
+</testsuites>"""
+        expected = ["ClassOne.TestOne", "ClassTwo.TestTwo"]
+        self.assertEquals(expected, UnitTestResults.results_from_string(multiple_failures_per_test_xml))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/common/net/web.py b/Tools/Scripts/webkitpy/common/net/web.py
new file mode 100644
index 0000000..b8a06e5
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/web.py
@@ -0,0 +1,36 @@
+# Copyright (C) 2011 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 urllib2
+
+from webkitpy.common.net.networktransaction import NetworkTransaction
+
+
+class Web(object):
+    def get_binary(self, url, convert_404_to_None=False):
+        return NetworkTransaction(convert_404_to_None=convert_404_to_None).run(lambda: urllib2.urlopen(url).read())
diff --git a/Tools/Scripts/webkitpy/common/net/web_mock.py b/Tools/Scripts/webkitpy/common/net/web_mock.py
new file mode 100644
index 0000000..423573c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/net/web_mock.py
@@ -0,0 +1,55 @@
+# Copyright (C) 2011 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 StringIO
+
+
+class MockWeb(object):
+    def __init__(self):
+        self.urls_fetched = []
+
+    def get_binary(self, url, convert_404_to_None=False):
+        self.urls_fetched.append(url)
+        return "MOCK Web result, convert 404 to None=%s" % convert_404_to_None
+
+
+# FIXME: Classes which are using Browser probably want to use Web instead.
+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 StringIO.StringIO()
diff --git a/Tools/Scripts/webkitpy/common/newstringio.py b/Tools/Scripts/webkitpy/common/newstringio.py
new file mode 100644
index 0000000..f6d08ec
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/newstringio.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""'with'-compliant StringIO implementation."""
+
+import StringIO
+
+
+class StringIO(StringIO.StringIO):
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        pass
diff --git a/Tools/Scripts/webkitpy/common/newstringio_unittest.py b/Tools/Scripts/webkitpy/common/newstringio_unittest.py
new file mode 100644
index 0000000..1ee2fb9
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/newstringio_unittest.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Unit tests for newstringio module."""
+
+import unittest
+
+import newstringio
+
+
+class NewStringIOTest(unittest.TestCase):
+    def test_with(self):
+        with newstringio.StringIO("foo") as f:
+            contents = f.read()
+        self.assertEqual(contents, "foo")
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/common/prettypatch.py b/Tools/Scripts/webkitpy/common/prettypatch.py
new file mode 100644
index 0000000..e8a913a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/prettypatch.py
@@ -0,0 +1,67 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import tempfile
+
+
+class PrettyPatch(object):
+    # FIXME: PrettyPatch should not require checkout_root.
+    def __init__(self, executive, checkout_root):
+        self._executive = executive
+        self._checkout_root = checkout_root
+
+    def pretty_diff_file(self, diff):
+        # Diffs can contain multiple text files of different encodings
+        # so we always deal with them as byte arrays, not unicode strings.
+        assert(isinstance(diff, str))
+        pretty_diff = self.pretty_diff(diff)
+        diff_file = tempfile.NamedTemporaryFile(suffix=".html")
+        diff_file.write(pretty_diff)
+        diff_file.flush()
+        return diff_file
+
+    def pretty_diff(self, diff):
+        # pretify.rb will hang forever if given no input.
+        # Avoid the hang by returning an empty string.
+        if not diff:
+            return ""
+
+        pretty_patch_path = os.path.join(self._checkout_root,
+                                         "Websites", "bugs.webkit.org",
+                                         "PrettyPatch")
+        prettify_path = os.path.join(pretty_patch_path, "prettify.rb")
+        args = [
+            "ruby",
+            "-I",
+            pretty_patch_path,
+            prettify_path,
+        ]
+        # PrettyPatch does not modify the encoding of the diff output
+        # so we can't expect it to be utf-8.
+        return self._executive.run_command(args, input=diff, decode_output=False)
diff --git a/Tools/Scripts/webkitpy/common/prettypatch_unittest.py b/Tools/Scripts/webkitpy/common/prettypatch_unittest.py
new file mode 100644
index 0000000..37fa844
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/prettypatch_unittest.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.
+
+import os.path
+import sys
+import unittest
+
+from webkitpy.common.system.executive import Executive
+from webkitpy.common.prettypatch import PrettyPatch
+
+
+class PrettyPatchTest(unittest.TestCase):
+    def check_ruby(self):
+        executive = Executive()
+        try:
+            result = executive.run_command(['ruby', '--version'])
+        except OSError, e:
+            return False
+        return True
+
+    _diff_with_multiple_encodings = """
+Index: utf8_test
+===================================================================
+--- utf8_test\t(revision 0)
++++ utf8_test\t(revision 0)
+@@ -0,0 +1 @@
++utf-8 test: \xc2\xa0
+Index: latin1_test
+===================================================================
+--- latin1_test\t(revision 0)
++++ latin1_test\t(revision 0)
+@@ -0,0 +1 @@
++latin1 test: \xa0
+"""
+
+    def _webkit_root(self):
+        webkitpy_common = os.path.dirname(__file__)
+        webkitpy = os.path.dirname(webkitpy_common)
+        scripts = os.path.dirname(webkitpy)
+        webkit_tools = os.path.dirname(scripts)
+        webkit_root = os.path.dirname(webkit_tools)
+        return webkit_root
+
+    def test_pretty_diff_encodings(self):
+        if not self.check_ruby():
+            return
+
+        if sys.platform == 'win32':
+            # FIXME: disabled due to https://bugs.webkit.org/show_bug.cgi?id=93192
+            return
+
+        pretty_patch = PrettyPatch(Executive(), self._webkit_root())
+        pretty = pretty_patch.pretty_diff(self._diff_with_multiple_encodings)
+        self.assertTrue(pretty)  # We got some output
+        self.assertTrue(isinstance(pretty, str))  # It's a byte array, not unicode
+
+    def test_pretty_print_empty_string(self):
+        if not self.check_ruby():
+            return
+
+        # Make sure that an empty diff does not hang the process.
+        pretty_patch = PrettyPatch(Executive(), self._webkit_root())
+        self.assertEqual(pretty_patch.pretty_diff(""), "")
diff --git a/Tools/Scripts/webkitpy/common/read_checksum_from_png.py b/Tools/Scripts/webkitpy/common/read_checksum_from_png.py
new file mode 100644
index 0000000..70a0502
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/read_checksum_from_png.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# Copyright (c) 2011 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.
+
+
+def read_checksum(filehandle):
+    # We expect the comment to be at the beginning of the file.
+    data = filehandle.read(2048)
+    comment_key = 'tEXtchecksum\x00'
+    comment_pos = data.find(comment_key)
+    if comment_pos == -1:
+        return
+
+    checksum_pos = comment_pos + len(comment_key)
+    return data[checksum_pos:checksum_pos + 32]
diff --git a/Tools/Scripts/webkitpy/common/read_checksum_from_png_unittest.py b/Tools/Scripts/webkitpy/common/read_checksum_from_png_unittest.py
new file mode 100644
index 0000000..defbbf8
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/read_checksum_from_png_unittest.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+# Copyright (C) 2011 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:
+#
+# 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.
+
+import StringIO
+import unittest
+from webkitpy.common import read_checksum_from_png
+
+
+class ReadChecksumFromPngTest(unittest.TestCase):
+    def test_read_checksum(self):
+        # Test a file with the comment.
+        filehandle = StringIO.StringIO('''\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x03 \x00\x00\x02X\x08\x02\x00\x00\x00\x15\x14\x15'\x00\x00\x00)tEXtchecksum\x003c4134fe2739880353f91c5b84cadbaaC\xb8?\xec\x00\x00\x16\xfeIDATx\x9c\xed\xdd[\x8cU\xe5\xc1\xff\xf15T\x18\x0ea,)\xa6\x80XZ<\x10\n\xd6H\xc4V\x88}\xb5\xa9\xd6r\xd5\x0bki0\xa6\xb5ih\xd2\xde\x98PHz\xd1\x02=\\q#\x01\x8b\xa5rJ\x8b\x88i\xacM\xc5h\x8cbMk(\x1ez@!\x0c\xd5\xd2\xc2\xb44\x1c\x848\x1dF(\xeb\x7f\xb1\xff\xd9\xef~g\xd6\xde3\xe0o\x10\xec\xe7sa6{\xd6z\xd6\xb3\xd7\xf3\xa8_7\xdbM[Y\x96\x05\x00\x009\xc3\xde\xeb\t\x00\x00\xbc\xdf\x08,\x00\x800\x81\x05\x00\x10&\xb0\x00\x00\xc2\x04\x16\x00@\x98\xc0\x02\x00\x08\x13X\x00\x00a\x02\x0b\x00 Lx01\x00\x84\t,\x00\x800\x81\x05\x00\x10\xd64\xb0\xda\x9a\xdb\xb6m\xdb\xb4i\xd3\xfa\x9fr\xf3\xcd7\x0f\xe5T\x07\xe5\xd4\xa9''')
+        checksum = read_checksum_from_png.read_checksum(filehandle)
+        self.assertEquals('3c4134fe2739880353f91c5b84cadbaa', checksum)
+
+        # Test a file without the comment.
+        filehandle = StringIO.StringIO('''\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x03 \x00\x00\x02X\x08\x02\x00\x00\x00\x15\x14\x15'\x00\x00\x16\xfeIDATx\x9c\xed\xdd[\x8cU\xe5\xc1\xff\xf15T\x18\x0ea,)\xa6\x80XZ<\x10\n\xd6H\xc4V\x88}\xb5\xa9\xd6r\xd5\x0bki0\xa6\xb5ih\xd2\xde\x98PHz\xd1\x02=\\q#\x01\x8b\xa5rJ\x8b\x88i\xacM\xc5h\x8cbMk(\x1ez@!\x0c\xd5\xd2\xc2\xb44\x1c\x848\x1dF(\xeb\x7f\xb1\xff\xd9\xef~g\xd6\xde3\xe0o\x10\xec\xe7sa6{\xd6z\xd6\xb3\xd7\xf3\xa8_7\xdbM[Y\x96\x05\x00\x009\xc3\xde\xeb\t\x00\x00\xbc\xdf\x08,\x00\x800\x81\x05\x00\x10&\xb0\x00\x00\xc2\x04\x16\x00@\x98\xc0\x02\x00\x08\x13X\x00\x00a\x02\x0b\x00 Lx01\x00\x84\t,\x00\x800\x81\x05\x00\x10\xd64\xb0\xda\x9a\xdb\xb6m\xdb\xb4i\xd3\xfa\x9fr\xf3\xcd7\x0f\xe5T\x07\xe5\xd4\xa9S\x8b\x17/\x1e?~\xfc\xf8\xf1\xe3\xef\xbf\xff\xfe\xf7z:M5\xbb\x87\x17\xcbUZ\x8f|V\xd7\xbd\x10\xb6\xcd{b\x88\xf6j\xb3\x9b?\x14\x9b\xa1>\xe6\xf9\xd9\xcf\x00\x17\x93''')
+        checksum = read_checksum_from_png.read_checksum(filehandle)
+        self.assertEquals(None, checksum)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/common/system/__init__.py b/Tools/Scripts/webkitpy/common/system/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/common/system/autoinstall.py b/Tools/Scripts/webkitpy/common/system/autoinstall.py
new file mode 100755
index 0000000..f3045f8
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/autoinstall.py
@@ -0,0 +1,414 @@
+# Copyright (c) 2009, Daniel Krech All rights reserved.
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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 the Daniel Krech 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
+# HOLDER 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.
+
+"""Support for automatically downloading Python packages from an URL."""
+
+
+import codecs
+import logging
+import os
+import shutil
+import sys
+import tarfile
+import tempfile
+import urllib
+import urlparse
+import zipfile
+
+_log = logging.getLogger(__name__)
+
+
+class AutoInstaller(object):
+
+    """Supports automatically installing Python packages from an URL.
+
+    Supports uncompressed files, .tar.gz, and .zip formats.
+
+    Basic usage:
+
+    installer = AutoInstaller()
+
+    installer.install(url="http://pypi.python.org/packages/source/p/pep8/pep8-0.5.0.tar.gz#md5=512a818af9979290cd619cce8e9c2e2b",
+                      url_subpath="pep8-0.5.0/pep8.py")
+    installer.install(url="http://pypi.python.org/packages/source/m/mechanize/mechanize-0.2.4.zip",
+                      url_subpath="mechanize")
+
+    """
+
+    def __init__(self, append_to_search_path=False, make_package=True,
+                 target_dir=None, temp_dir=None):
+        """Create an AutoInstaller instance, and set up the target directory.
+
+        Args:
+          append_to_search_path: A boolean value of whether to append the
+                                 target directory to the sys.path search path.
+          make_package: A boolean value of whether to make the target
+                        directory a package.  This adds an __init__.py file
+                        to the target directory -- allowing packages and
+                        modules within the target directory to be imported
+                        explicitly using dotted module names.
+          target_dir: The directory path to which packages should be installed.
+                      Defaults to a subdirectory of the folder containing
+                      this module called "autoinstalled".
+          temp_dir: The directory path to use for any temporary files
+                    generated while downloading, unzipping, and extracting
+                    packages to install.  Defaults to a standard temporary
+                    location generated by the tempfile module.  This
+                    parameter should normally be used only for development
+                    testing.
+
+        """
+        if target_dir is None:
+            this_dir = os.path.dirname(__file__)
+            target_dir = os.path.join(this_dir, "autoinstalled")
+
+        # Ensure that the target directory exists.
+        self._set_up_target_dir(target_dir, append_to_search_path, make_package)
+
+        self._target_dir = target_dir
+        self._temp_dir = temp_dir
+
+    def _write_file(self, path, text, encoding):
+        with codecs.open(path, "w", encoding) as filehandle:
+            filehandle.write(text)
+
+    def _set_up_target_dir(self, target_dir, append_to_search_path,
+                           make_package):
+        """Set up a target directory.
+
+        Args:
+          target_dir: The path to the target directory to set up.
+          append_to_search_path: A boolean value of whether to append the
+                                 target directory to the sys.path search path.
+          make_package: A boolean value of whether to make the target
+                        directory a package.  This adds an __init__.py file
+                        to the target directory -- allowing packages and
+                        modules within the target directory to be imported
+                        explicitly using dotted module names.
+
+        """
+        if not os.path.exists(target_dir):
+            os.makedirs(target_dir)
+
+        if append_to_search_path:
+            sys.path.append(target_dir)
+
+        if make_package:
+            self._make_package(target_dir)
+
+    def _make_package(self, target_dir):
+        init_path = os.path.join(target_dir, "__init__.py")
+        if not os.path.exists(init_path):
+            text = ("# This file is required for Python to search this "
+                    "directory for modules.\n")
+            self._write_file(init_path, text, "ascii")
+
+    def _create_scratch_directory_inner(self, prefix):
+        """Create a scratch directory without exception handling.
+
+        Creates a scratch directory inside the AutoInstaller temp
+        directory self._temp_dir, or inside a platform-dependent temp
+        directory if self._temp_dir is None.  Returns the path to the
+        created scratch directory.
+
+        Raises:
+          OSError: [Errno 2] if the containing temp directory self._temp_dir
+                             is not None and does not exist.
+
+        """
+        # The tempfile.mkdtemp() method function requires that the
+        # directory corresponding to the "dir" parameter already exist
+        # if it is not None.
+        scratch_dir = tempfile.mkdtemp(prefix=prefix, dir=self._temp_dir)
+        return scratch_dir
+
+    def _create_scratch_directory(self, target_name):
+        """Create a temporary scratch directory, and return its path.
+
+        The scratch directory is generated inside the temp directory
+        of this AutoInstaller instance.  This method also creates the
+        temp directory if it does not already exist.
+
+        """
+        prefix = target_name.replace(os.sep, "_") + "_"
+        try:
+            scratch_dir = self._create_scratch_directory_inner(prefix)
+        except OSError:
+            # Handle case of containing temp directory not existing--
+            # OSError: [Errno 2] No such file or directory:...
+            temp_dir = self._temp_dir
+            if temp_dir is None or os.path.exists(temp_dir):
+                raise
+            # Else try again after creating the temp directory.
+            os.makedirs(temp_dir)
+            scratch_dir = self._create_scratch_directory_inner(prefix)
+
+        return scratch_dir
+
+    def _url_downloaded_path(self, target_name):
+        return os.path.join(self._target_dir, ".%s.url" % target_name)
+
+    def _is_downloaded(self, target_name, url):
+        version_path = self._url_downloaded_path(target_name)
+
+        if not os.path.exists(version_path):
+            return False
+
+        with codecs.open(version_path, "r", "utf-8") as filehandle:
+            return filehandle.read().strip() == url.strip()
+
+    def _record_url_downloaded(self, target_name, url):
+        version_path = self._url_downloaded_path(target_name)
+        self._write_file(version_path, url, "utf-8")
+
+    def _extract_targz(self, path, scratch_dir):
+        # tarfile.extractall() extracts to a path without the trailing ".tar.gz".
+        target_basename = os.path.basename(path[:-len(".tar.gz")])
+        target_path = os.path.join(scratch_dir, target_basename)
+
+        try:
+            tar_file = tarfile.open(path)
+        except tarfile.ReadError, err:
+            # Append existing Error message to new Error.
+            message = ("Could not open tar file: %s\n"
+                       " The file probably does not have the correct format.\n"
+                       " --> Inner message: %s"
+                       % (path, err))
+            raise Exception(message)
+
+        try:
+            tar_file.extractall(target_path)
+        finally:
+            tar_file.close()
+
+        return target_path
+
+    # This is a replacement for ZipFile.extractall(), which is
+    # available in Python 2.6 but not in earlier versions.
+    # NOTE: The version in 2.6.1 (which shipped on Snow Leopard) is broken!
+    def _extract_all(self, zip_file, target_dir):
+        for name in zip_file.namelist():
+            path = os.path.join(target_dir, name)
+            if not os.path.basename(path):
+                # Then the path ends in a slash, so it is a directory.
+                os.makedirs(path)
+                continue
+
+            try:
+                # We open this file w/o encoding, as we're reading/writing
+                # the raw byte-stream from the zip file.
+                outfile = open(path, 'wb')
+            except IOError:
+                # Not all zip files seem to list the directories explicitly,
+                # so try again after creating the containing directory.
+                _log.debug("Got IOError: retrying after creating directory...")
+                dirname = os.path.dirname(path)
+                os.makedirs(dirname)
+                outfile = open(path, 'wb')
+
+            try:
+                outfile.write(zip_file.read(name))
+            finally:
+                outfile.close()
+
+    def _unzip(self, path, scratch_dir):
+        # zipfile.extractall() extracts to a path without the trailing ".zip".
+        target_basename = os.path.basename(path[:-len(".zip")])
+        target_path = os.path.join(scratch_dir, target_basename)
+
+        try:
+            zip_file = zipfile.ZipFile(path, "r")
+        except zipfile.BadZipfile, err:
+            message = ("Could not open zip file: %s\n"
+                       " --> Inner message: %s"
+                       % (path, err))
+            raise Exception(message)
+
+        try:
+            self._extract_all(zip_file, scratch_dir)
+        finally:
+            zip_file.close()
+
+        return target_path
+
+    def _prepare_package(self, path, scratch_dir):
+        """Prepare a package for use, if necessary, and return the new path.
+
+        For example, this method unzips zipped files and extracts
+        tar files.
+
+        Args:
+          path: The path to the downloaded URL contents.
+          scratch_dir: The scratch directory.  Note that the scratch
+                       directory contains the file designated by the
+                       path parameter.
+
+        """
+        # FIXME: Add other natural extensions.
+        if path.endswith(".zip"):
+            new_path = self._unzip(path, scratch_dir)
+        elif path.endswith(".tar.gz"):
+            new_path = self._extract_targz(path, scratch_dir)
+        else:
+            # No preparation is needed.
+            new_path = path
+
+        return new_path
+
+    def _download_to_stream(self, url, stream):
+        try:
+            netstream = urllib.urlopen(url)
+        except IOError, err:
+            # Append existing Error message to new Error.
+            message = ('Could not download Python modules from URL "%s".\n'
+                       " Make sure you are connected to the internet.\n"
+                       " You must be connected to the internet when "
+                       "downloading needed modules for the first time.\n"
+                       " --> Inner message: %s"
+                       % (url, err))
+            raise IOError(message)
+        code = 200
+        if hasattr(netstream, "getcode"):
+            code = netstream.getcode()
+        if not 200 <= code < 300:
+            raise ValueError("HTTP Error code %s" % code)
+
+        BUFSIZE = 2**13  # 8KB
+        while True:
+            data = netstream.read(BUFSIZE)
+            if not data:
+                break
+            stream.write(data)
+        netstream.close()
+
+    def _download(self, url, scratch_dir):
+        url_path = urlparse.urlsplit(url)[2]
+        url_path = os.path.normpath(url_path)  # Removes trailing slash.
+        target_filename = os.path.basename(url_path)
+        target_path = os.path.join(scratch_dir, target_filename)
+
+        with open(target_path, "wb") as stream:
+            self._download_to_stream(url, stream)
+
+        return target_path
+
+    def _install(self, scratch_dir, package_name, target_path, url,
+                 url_subpath):
+        """Install a python package from an URL.
+
+        This internal method overwrites the target path if the target
+        path already exists.
+
+        """
+        path = self._download(url=url, scratch_dir=scratch_dir)
+        path = self._prepare_package(path, scratch_dir)
+
+        if url_subpath is None:
+            source_path = path
+        else:
+            source_path = os.path.join(path, url_subpath)
+
+        if os.path.exists(target_path):
+            if os.path.isdir(target_path):
+                shutil.rmtree(target_path)
+            else:
+                os.remove(target_path)
+
+        # shutil.move() command creates intermediate directories if they do not exist.
+        shutil.move(source_path, target_path)
+
+        # ensure all the new directories are importable.
+        intermediate_dirs = os.path.dirname(os.path.relpath(target_path, self._target_dir))
+        parent_dirname = self._target_dir
+        for dirname in intermediate_dirs.split(os.sep):
+            parent_dirname = os.path.join(parent_dirname, dirname)
+            self._make_package(parent_dirname)
+
+        self._record_url_downloaded(package_name, url)
+
+    def install(self, url, should_refresh=False, target_name=None,
+                url_subpath=None):
+        """Install a python package from an URL.
+
+        Args:
+          url: The URL from which to download the package.
+
+        Optional Args:
+          should_refresh: A boolean value of whether the package should be
+                          downloaded again if the package is already present.
+          target_name: The name of the folder or file in the autoinstaller
+                       target directory at which the package should be
+                       installed.  Defaults to the base name of the
+                       URL sub-path.  This parameter must be provided if
+                       the URL sub-path is not specified.
+          url_subpath: The relative path of the URL directory that should
+                       be installed.  Defaults to the full directory, or
+                       the entire URL contents.
+
+        """
+        if target_name is None:
+            if not url_subpath:
+                raise ValueError('The "target_name" parameter must be '
+                                 'provided if the "url_subpath" parameter '
+                                 "is not provided.")
+            # Remove any trailing slashes.
+            url_subpath = os.path.normpath(url_subpath)
+            target_name = os.path.basename(url_subpath)
+
+        target_path = os.path.join(self._target_dir, target_name)
+        if not should_refresh and self._is_downloaded(target_name, url):
+            return False
+
+        package_name = target_name.replace(os.sep, '.')
+        _log.info("Auto-installing package: %s" % package_name)
+
+        # The scratch directory is where we will download and prepare
+        # files specific to this install until they are ready to move
+        # into place.
+        scratch_dir = self._create_scratch_directory(target_name)
+
+        try:
+            self._install(package_name=package_name,
+                          target_path=target_path,
+                          scratch_dir=scratch_dir,
+                          url=url,
+                          url_subpath=url_subpath)
+        except Exception, err:
+            # Append existing Error message to new Error.
+            message = ("Error auto-installing the %s package to:\n"
+                       ' "%s"\n'
+                       " --> Inner message: %s"
+                       % (target_name, target_path, err))
+            raise Exception(message)
+        finally:
+            shutil.rmtree(scratch_dir)
+        _log.debug('Auto-installed %s to:' % url)
+        _log.debug('    "%s"' % target_path)
+        return True
diff --git a/Tools/Scripts/webkitpy/common/system/crashlogs.py b/Tools/Scripts/webkitpy/common/system/crashlogs.py
new file mode 100644
index 0000000..270ca81
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/crashlogs.py
@@ -0,0 +1,74 @@
+# Copyright (c) 2011, 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 re
+
+
+class CrashLogs(object):
+    def __init__(self, host):
+        self._host = host
+
+    def find_newest_log(self, process_name, pid=None, include_errors=False, newer_than=None):
+        if self._host.platform.is_mac():
+            return self._find_newest_log_darwin(process_name, pid, include_errors, newer_than)
+        return None
+
+    def _log_directory_darwin(self):
+        log_directory = self._host.filesystem.expanduser("~")
+        log_directory = self._host.filesystem.join(log_directory, "Library", "Logs")
+        if self._host.filesystem.exists(self._host.filesystem.join(log_directory, "DiagnosticReports")):
+            log_directory = self._host.filesystem.join(log_directory, "DiagnosticReports")
+        else:
+            log_directory = self._host.filesystem.join(log_directory, "CrashReporter")
+        return log_directory
+
+    def _find_newest_log_darwin(self, process_name, pid, include_errors, newer_than):
+        def is_crash_log(fs, dirpath, basename):
+            return basename.startswith(process_name + "_") and basename.endswith(".crash")
+
+        log_directory = self._log_directory_darwin()
+        logs = self._host.filesystem.files_under(log_directory, file_filter=is_crash_log)
+        first_line_regex = re.compile(r'^Process:\s+(?P<process_name>.*) \[(?P<pid>\d+)\]$')
+        errors = ''
+        for path in reversed(sorted(logs)):
+            try:
+                if not newer_than or self._host.filesystem.mtime(path) > newer_than:
+                    f = self._host.filesystem.read_text_file(path)
+                    match = first_line_regex.match(f[0:f.find('\n')])
+                    if match and match.group('process_name') == process_name and (pid is None or int(match.group('pid')) == pid):
+                        return errors + f
+            except IOError, e:
+                if include_errors:
+                    errors += "ERROR: Failed to read '%s': %s\n" % (path, str(e))
+            except OSError, e:
+                if include_errors:
+                    errors += "ERROR: Failed to read '%s': %s\n" % (path, str(e))
+
+        if include_errors and errors:
+            return errors
+        return None
diff --git a/Tools/Scripts/webkitpy/common/system/crashlogs_unittest.py b/Tools/Scripts/webkitpy/common/system/crashlogs_unittest.py
new file mode 100644
index 0000000..1f5c40a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/crashlogs_unittest.py
@@ -0,0 +1,122 @@
+# Copyright (C) 2011 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:
+#
+# 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.
+
+import unittest
+
+from webkitpy.common.system.crashlogs import CrashLogs
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.systemhost import SystemHost
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+from webkitpy.thirdparty.mock import Mock
+
+
+def make_mock_crash_report_darwin(process_name, pid):
+    return """Process:         {process_name} [{pid}]
+Path:            /Volumes/Data/slave/snowleopard-intel-release-tests/build/WebKitBuild/Release/{process_name}
+Identifier:      {process_name}
+Version:         ??? (???)
+Code Type:       X86-64 (Native)
+Parent Process:  Python [2578]
+
+Date/Time:       2011-12-07 13:27:34.816 -0800
+OS Version:      Mac OS X 10.6.8 (10K549)
+Report Version:  6
+
+Interval Since Last Report:          1660 sec
+Crashes Since Last Report:           1
+Per-App Crashes Since Last Report:   1
+Anonymous UUID:                      507D4EEB-9D70-4E2E-B322-2D2F0ABFEDC0
+
+Exception Type:  EXC_BREAKPOINT (SIGTRAP)
+Exception Codes: 0x0000000000000002, 0x0000000000000000
+Crashed Thread:  0
+
+Dyld Error Message:
+  Library not loaded: /Volumes/Data/WebKit-BuildSlave/snowleopard-intel-release/build/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore
+  Referenced from: /Volumes/Data/slave/snowleopard-intel-release/build/WebKitBuild/Release/WebKit.framework/Versions/A/WebKit
+  Reason: image not found
+
+Binary Images:
+    0x7fff5fc00000 -     0x7fff5fc3be0f  dyld 132.1 (???) <29DECB19-0193-2575-D838-CF743F0400B2> /usr/lib/dyld
+
+System Profile:
+Model: Xserve3,1, BootROM XS31.0081.B04, 8 processors, Quad-Core Intel Xeon, 2.26 GHz, 6 GB, SMC 1.43f4
+Graphics: NVIDIA GeForce GT 120, NVIDIA GeForce GT 120, PCIe, 256 MB
+Memory Module: global_name
+Network Service: Ethernet 2, Ethernet, en1
+PCI Card: NVIDIA GeForce GT 120, sppci_displaycontroller, MXM-Slot
+Serial ATA Device: OPTIARC DVD RW AD-5670S
+""".format(process_name=process_name, pid=pid)
+
+class CrashLogsTest(unittest.TestCase):
+    def assertLinesEqual(self, a, b):
+        if hasattr(self, 'assertMultiLineEqual'):
+            self.assertMultiLineEqual(a, b)
+        else:
+            self.assertEqual(a.splitlines(), b.splitlines())
+
+
+    def test_find_log_darwin(self):
+        if not SystemHost().platform.is_mac():
+            return
+
+        older_mock_crash_report = make_mock_crash_report_darwin('DumpRenderTree', 28528)
+        mock_crash_report = make_mock_crash_report_darwin('DumpRenderTree', 28530)
+        newer_mock_crash_report = make_mock_crash_report_darwin('DumpRenderTree', 28529)
+        other_process_mock_crash_report = make_mock_crash_report_darwin('FooProcess', 28527)
+        misformatted_mock_crash_report = 'Junk that should not appear in a crash report' + make_mock_crash_report_darwin('DumpRenderTree', 28526)[200:]
+        files = {}
+        files['/Users/mock/Library/Logs/DiagnosticReports/DumpRenderTree_2011-06-13-150718_quadzen.crash'] = older_mock_crash_report
+        files['/Users/mock/Library/Logs/DiagnosticReports/DumpRenderTree_2011-06-13-150719_quadzen.crash'] = mock_crash_report
+        files['/Users/mock/Library/Logs/DiagnosticReports/DumpRenderTree_2011-06-13-150720_quadzen.crash'] = newer_mock_crash_report
+        files['/Users/mock/Library/Logs/DiagnosticReports/DumpRenderTree_2011-06-13-150721_quadzen.crash'] = None
+        files['/Users/mock/Library/Logs/DiagnosticReports/DumpRenderTree_2011-06-13-150722_quadzen.crash'] = other_process_mock_crash_report
+        files['/Users/mock/Library/Logs/DiagnosticReports/DumpRenderTree_2011-06-13-150723_quadzen.crash'] = misformatted_mock_crash_report
+        filesystem = MockFileSystem(files)
+        crash_logs = CrashLogs(MockSystemHost(filesystem=filesystem))
+        log = crash_logs.find_newest_log("DumpRenderTree")
+        self.assertLinesEqual(log, newer_mock_crash_report)
+        log = crash_logs.find_newest_log("DumpRenderTree", 28529)
+        self.assertLinesEqual(log, newer_mock_crash_report)
+        log = crash_logs.find_newest_log("DumpRenderTree", 28530)
+        self.assertLinesEqual(log, mock_crash_report)
+        log = crash_logs.find_newest_log("DumpRenderTree", 28531)
+        self.assertEqual(log, None)
+        log = crash_logs.find_newest_log("DumpRenderTree", newer_than=1.0)
+        self.assertEqual(log, None)
+
+        def bad_read(path):
+            raise IOError('IOError: No such file or directory')
+
+        def bad_mtime(path):
+            raise OSError('OSError: No such file or directory')
+
+        filesystem.read_text_file = bad_read
+        log = crash_logs.find_newest_log("DumpRenderTree", 28531, include_errors=True)
+        self.assertTrue('IOError: No such file or directory' in log)
+
+        filesystem = MockFileSystem(files)
+        crash_logs = CrashLogs(MockSystemHost(filesystem=filesystem))
+        filesystem.mtime = bad_mtime
+        log = crash_logs.find_newest_log("DumpRenderTree", newer_than=1.0, include_errors=True)
+        self.assertTrue('OSError: No such file or directory' in log)
diff --git a/Tools/Scripts/webkitpy/common/system/deprecated_logging.py b/Tools/Scripts/webkitpy/common/system/deprecated_logging.py
new file mode 100644
index 0000000..1375354
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/deprecated_logging.py
@@ -0,0 +1,91 @@
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 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:
+# 
+#     * 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.
+#
+# WebKit's Python module for logging
+# This module is now deprecated in favor of python's built-in logging.py.
+
+import codecs
+import os
+import sys
+
+
+def log(string):
+    print >> sys.stderr, string
+
+
+def error(string):
+    log("ERROR: %s" % string)
+    sys.exit(1)
+
+
+# Simple class to split output between multiple destinations
+class tee:
+    def __init__(self, *files):
+        self.files = files
+
+    # Callers should pass an already encoded string for writing.
+    def write(self, bytes):
+        for file in self.files:
+            file.write(bytes)
+
+
+class OutputTee:
+    def __init__(self):
+        self._original_stdout = None
+        self._original_stderr = None
+        self._files_for_output = []
+
+    def add_log(self, path):
+        log_file = self._open_log_file(path)
+        self._files_for_output.append(log_file)
+        self._tee_outputs_to_files(self._files_for_output)
+        return log_file
+
+    def remove_log(self, log_file):
+        self._files_for_output.remove(log_file)
+        self._tee_outputs_to_files(self._files_for_output)
+        log_file.close()
+
+    @staticmethod
+    def _open_log_file(log_path):
+        (log_directory, log_name) = os.path.split(log_path)
+        if log_directory and not os.path.exists(log_directory):
+            os.makedirs(log_directory)
+        return codecs.open(log_path, "a+", "utf-8")
+
+    def _tee_outputs_to_files(self, files):
+        if not self._original_stdout:
+            self._original_stdout = sys.stdout
+            self._original_stderr = sys.stderr
+        if files and len(files):
+            sys.stdout = tee(self._original_stdout, *files)
+            sys.stderr = tee(self._original_stderr, *files)
+        else:
+            sys.stdout = self._original_stdout
+            sys.stderr = self._original_stderr
diff --git a/Tools/Scripts/webkitpy/common/system/deprecated_logging_unittest.py b/Tools/Scripts/webkitpy/common/system/deprecated_logging_unittest.py
new file mode 100644
index 0000000..3778162
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/deprecated_logging_unittest.py
@@ -0,0 +1,60 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import StringIO
+import tempfile
+import unittest
+
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.system.deprecated_logging import *
+
+class LoggingTest(unittest.TestCase):
+
+    def assert_log_equals(self, log_input, expected_output):
+        original_stderr = sys.stderr
+        test_stderr = StringIO.StringIO()
+        sys.stderr = test_stderr
+
+        try:
+            log(log_input)
+            actual_output = test_stderr.getvalue()
+        finally:
+            sys.stderr = original_stderr
+
+        self.assertEquals(actual_output, expected_output, "log(\"%s\") expected: %s actual: %s" % (log_input, expected_output, actual_output))
+
+    def test_log(self):
+        self.assert_log_equals("test", "test\n")
+
+        # Test that log() does not throw an exception when passed an object instead of a string.
+        self.assert_log_equals(ScriptError(message="ScriptError"), "ScriptError\n")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/common/system/environment.py b/Tools/Scripts/webkitpy/common/system/environment.py
new file mode 100644
index 0000000..cd34048
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/environment.py
@@ -0,0 +1,41 @@
+# Copyright (C) 2011 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 Environment(object):
+    def __init__(self, env=None):
+        self.env = env or {}
+
+    def to_dictionary(self):
+        return self.env
+
+    def disable_gcc_smartquotes(self):
+        # Technically we only need to set LC_CTYPE to disable current
+        # smartquote behavior: http://gcc.gnu.org/bugzilla/show_bug.cgi?id=38363
+        # Apple's XCode sets LC_ALL instead, probably to be future-proof.
+        self.env['LC_ALL'] = 'C'
diff --git a/Tools/Scripts/webkitpy/common/system/environment_unittest.py b/Tools/Scripts/webkitpy/common/system/environment_unittest.py
new file mode 100644
index 0000000..6558b51
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/environment_unittest.py
@@ -0,0 +1,39 @@
+# Copyright (C) 2011 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 .environment import Environment
+
+
+class EnvironmentTest(unittest.TestCase):
+    def test_disable_gcc_smartquotes(self):
+        environment = Environment({})
+        environment.disable_gcc_smartquotes()
+        env = environment.to_dictionary()
+        self.assertEqual(env['LC_ALL'], 'C')
diff --git a/Tools/Scripts/webkitpy/common/system/executive.py b/Tools/Scripts/webkitpy/common/system/executive.py
new file mode 100644
index 0000000..b1d2390
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/executive.py
@@ -0,0 +1,481 @@
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 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:
+#
+#     * 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 errno
+import logging
+import multiprocessing
+import os
+import StringIO
+import signal
+import subprocess
+import sys
+import time
+
+from webkitpy.common.system.deprecated_logging import tee
+from webkitpy.common.system.filesystem import FileSystem
+
+
+_log = logging.getLogger(__name__)
+
+
+class ScriptError(Exception):
+
+    # This is a custom List.__str__ implementation to allow size limiting.
+    def _string_from_args(self, args, limit=100):
+        args_string = unicode(args)
+        # We could make this much fancier, but for now this is OK.
+        if len(args_string) > limit:
+            return args_string[:limit - 3] + "..."
+        return args_string
+
+    def __init__(self,
+                 message=None,
+                 script_args=None,
+                 exit_code=None,
+                 output=None,
+                 cwd=None):
+        if not message:
+            message = 'Failed to run "%s"' % self._string_from_args(script_args)
+            if exit_code:
+                message += " exit_code: %d" % exit_code
+            if cwd:
+                message += " cwd: %s" % cwd
+
+        Exception.__init__(self, message)
+        self.script_args = script_args # 'args' is already used by Exception
+        self.exit_code = exit_code
+        self.output = output
+        self.cwd = cwd
+
+    def message_with_output(self, output_limit=500):
+        if self.output:
+            if output_limit and len(self.output) > output_limit:
+                return u"%s\n\nLast %s characters of output:\n%s" % \
+                    (self, output_limit, self.output[-output_limit:])
+            return u"%s\n\n%s" % (self, self.output)
+        return unicode(self)
+
+    def command_name(self):
+        command_path = self.script_args
+        if type(command_path) is list:
+            command_path = command_path[0]
+        return os.path.basename(command_path)
+
+
+class Executive(object):
+    PIPE = subprocess.PIPE
+    STDOUT = subprocess.STDOUT
+
+    def _should_close_fds(self):
+        # We need to pass close_fds=True to work around Python bug #2320
+        # (otherwise we can hang when we kill DumpRenderTree when we are running
+        # multiple threads). See http://bugs.python.org/issue2320 .
+        # Note that close_fds isn't supported on Windows, but this bug only
+        # shows up on Mac and Linux.
+        return sys.platform not in ('win32', 'cygwin')
+
+    def _run_command_with_teed_output(self, args, teed_output, **kwargs):
+        args = map(unicode, args)  # Popen will throw an exception if args are non-strings (like int())
+        args = map(self._encode_argument_if_needed, args)
+
+        child_process = self.popen(args,
+                                   stdout=self.PIPE,
+                                   stderr=self.STDOUT,
+                                   close_fds=self._should_close_fds(),
+                                   **kwargs)
+
+        # Use our own custom wait loop because Popen ignores a tee'd
+        # stderr/stdout.
+        # FIXME: This could be improved not to flatten output to stdout.
+        while True:
+            output_line = child_process.stdout.readline()
+            if output_line == "" and child_process.poll() != None:
+                # poll() is not threadsafe and can throw OSError due to:
+                # http://bugs.python.org/issue1731717
+                return child_process.poll()
+            # We assume that the child process wrote to us in utf-8,
+            # so no re-encoding is necessary before writing here.
+            teed_output.write(output_line)
+
+    # FIXME: Remove this deprecated method and move callers to run_command.
+    # FIXME: This method is a hack to allow running command which both
+    # capture their output and print out to stdin.  Useful for things
+    # like "build-webkit" where we want to display to the user that we're building
+    # but still have the output to stuff into a log file.
+    def run_and_throw_if_fail(self, args, quiet=False, decode_output=True, **kwargs):
+        # Cache the child's output locally so it can be used for error reports.
+        child_out_file = StringIO.StringIO()
+        tee_stdout = sys.stdout
+        if quiet:
+            dev_null = open(os.devnull, "w")  # FIXME: Does this need an encoding?
+            tee_stdout = dev_null
+        child_stdout = tee(child_out_file, tee_stdout)
+        exit_code = self._run_command_with_teed_output(args, child_stdout, **kwargs)
+        if quiet:
+            dev_null.close()
+
+        child_output = child_out_file.getvalue()
+        child_out_file.close()
+
+        if decode_output:
+            child_output = child_output.decode(self._child_process_encoding())
+
+        if exit_code:
+            raise ScriptError(script_args=args,
+                              exit_code=exit_code,
+                              output=child_output)
+        return child_output
+
+    def cpu_count(self):
+        return multiprocessing.cpu_count()
+
+    @staticmethod
+    def interpreter_for_script(script_path, fs=None):
+        fs = fs or FileSystem()
+        lines = fs.read_text_file(script_path).splitlines()
+        if not len(lines):
+            return None
+        first_line = lines[0]
+        if not first_line.startswith('#!'):
+            return None
+        if first_line.find('python') > -1:
+            return sys.executable
+        if first_line.find('perl') > -1:
+            return 'perl'
+        if first_line.find('ruby') > -1:
+            return 'ruby'
+        return None
+
+    @staticmethod
+    def shell_command_for_script(script_path, fs=None):
+        fs = fs or FileSystem()
+        # Win32 does not support shebang. We need to detect the interpreter ourself.
+        if sys.platform == 'win32':
+            interpreter = Executive.interpreter_for_script(script_path, fs)
+            if interpreter:
+                return [interpreter, script_path]
+        return [script_path]
+
+    def kill_process(self, pid):
+        """Attempts to kill the given pid.
+        Will fail silently if pid does not exist or insufficient permisssions."""
+        if sys.platform == "win32":
+            # We only use taskkill.exe on windows (not cygwin) because subprocess.pid
+            # is a CYGWIN pid and taskkill.exe expects a windows pid.
+            # Thankfully os.kill on CYGWIN handles either pid type.
+            command = ["taskkill.exe", "/f", "/pid", pid]
+            # taskkill will exit 128 if the process is not found.  We should log.
+            self.run_command(command, error_handler=self.ignore_error)
+            return
+
+        # According to http://docs.python.org/library/os.html
+        # os.kill isn't available on Windows. python 2.5.5 os.kill appears
+        # to work in cygwin, however it occasionally raises EAGAIN.
+        retries_left = 10 if sys.platform == "cygwin" else 1
+        while retries_left > 0:
+            try:
+                retries_left -= 1
+                os.kill(pid, signal.SIGKILL)
+                _ = os.waitpid(pid, os.WNOHANG)
+            except OSError, e:
+                if e.errno == errno.EAGAIN:
+                    if retries_left <= 0:
+                        _log.warn("Failed to kill pid %s.  Too many EAGAIN errors." % pid)
+                    continue
+                if e.errno == errno.ESRCH:  # The process does not exist.
+                    return
+                if e.errno == errno.EPIPE:  # The process has exited already on cygwin
+                    return
+                if e.errno == errno.ECHILD:
+                    # Can't wait on a non-child process, but the kill worked.
+                    return
+                if e.errno == errno.EACCES and sys.platform == 'cygwin':
+                    # Cygwin python sometimes can't kill native processes.
+                    return
+                raise
+
+    def _win32_check_running_pid(self, pid):
+        # importing ctypes at the top-level seems to cause weird crashes at
+        # exit under cygwin on apple's win port. Only win32 needs cygwin, so
+        # we import it here instead. See https://bugs.webkit.org/show_bug.cgi?id=91682
+        import ctypes
+
+        class PROCESSENTRY32(ctypes.Structure):
+            _fields_ = [("dwSize", ctypes.c_ulong),
+                        ("cntUsage", ctypes.c_ulong),
+                        ("th32ProcessID", ctypes.c_ulong),
+                        ("th32DefaultHeapID", ctypes.POINTER(ctypes.c_ulong)),
+                        ("th32ModuleID", ctypes.c_ulong),
+                        ("cntThreads", ctypes.c_ulong),
+                        ("th32ParentProcessID", ctypes.c_ulong),
+                        ("pcPriClassBase", ctypes.c_ulong),
+                        ("dwFlags", ctypes.c_ulong),
+                        ("szExeFile", ctypes.c_char * 260)]
+
+        CreateToolhelp32Snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot
+        Process32First = ctypes.windll.kernel32.Process32First
+        Process32Next = ctypes.windll.kernel32.Process32Next
+        CloseHandle = ctypes.windll.kernel32.CloseHandle
+        TH32CS_SNAPPROCESS = 0x00000002  # win32 magic number
+        hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
+        pe32 = PROCESSENTRY32()
+        pe32.dwSize = ctypes.sizeof(PROCESSENTRY32)
+        result = False
+        if not Process32First(hProcessSnap, ctypes.byref(pe32)):
+            _log.debug("Failed getting first process.")
+            CloseHandle(hProcessSnap)
+            return result
+        while True:
+            if pe32.th32ProcessID == pid:
+                result = True
+                break
+            if not Process32Next(hProcessSnap, ctypes.byref(pe32)):
+                break
+        CloseHandle(hProcessSnap)
+        return result
+
+    def check_running_pid(self, pid):
+        """Return True if pid is alive, otherwise return False."""
+        if sys.platform == 'win32':
+            return self._win32_check_running_pid(pid)
+
+        try:
+            os.kill(pid, 0)
+            return True
+        except OSError:
+            return False
+
+    def running_pids(self, process_name_filter=None):
+        if not process_name_filter:
+            process_name_filter = lambda process_name: True
+
+        running_pids = []
+
+        if sys.platform in ("win32", "cygwin"):
+            # FIXME: running_pids isn't implemented on Windows yet...
+            return []
+
+        ps_process = self.popen(['ps', '-eo', 'pid,comm'], stdout=self.PIPE, stderr=self.PIPE)
+        stdout, _ = ps_process.communicate()
+        for line in stdout.splitlines():
+            try:
+                # In some cases the line can contain one or more
+                # leading white-spaces, so strip it before split.
+                pid, process_name = line.strip().split(' ', 1)
+                if process_name_filter(process_name):
+                    running_pids.append(int(pid))
+            except ValueError, e:
+                pass
+
+        return sorted(running_pids)
+
+    def wait_newest(self, process_name_filter=None):
+        if not process_name_filter:
+            process_name_filter = lambda process_name: True
+
+        running_pids = self.running_pids(process_name_filter)
+        if not running_pids:
+            return
+        pid = running_pids[-1]
+
+        while self.check_running_pid(pid):
+            time.sleep(0.25)
+
+    def _windows_image_name(self, process_name):
+        name, extension = os.path.splitext(process_name)
+        if not extension:
+            # taskkill expects processes to end in .exe
+            # If necessary we could add a flag to disable appending .exe.
+            process_name = "%s.exe" % name
+        return process_name
+
+    def kill_all(self, process_name):
+        """Attempts to kill processes matching process_name.
+        Will fail silently if no process are found."""
+        if sys.platform in ("win32", "cygwin"):
+            image_name = self._windows_image_name(process_name)
+            command = ["taskkill.exe", "/f", "/im", image_name]
+            # taskkill will exit 128 if the process is not found.  We should log.
+            self.run_command(command, error_handler=self.ignore_error)
+            return
+
+        # FIXME: This is inconsistent that kill_all uses TERM and kill_process
+        # uses KILL.  Windows is always using /f (which seems like -KILL).
+        # We should pick one mode, or add support for switching between them.
+        # Note: Mac OS X 10.6 requires -SIGNALNAME before -u USER
+        command = ["killall", "-TERM", "-u", os.getenv("USER"), process_name]
+        # killall returns 1 if no process can be found and 2 on command error.
+        # FIXME: We should pass a custom error_handler to allow only exit_code 1.
+        # We should log in exit_code == 1
+        self.run_command(command, error_handler=self.ignore_error)
+
+    # Error handlers do not need to be static methods once all callers are
+    # updated to use an Executive object.
+
+    @staticmethod
+    def default_error_handler(error):
+        raise error
+
+    @staticmethod
+    def ignore_error(error):
+        pass
+
+    def _compute_stdin(self, input):
+        """Returns (stdin, string_to_communicate)"""
+        # FIXME: We should be returning /dev/null for stdin
+        # or closing stdin after process creation to prevent
+        # child processes from getting input from the user.
+        if not input:
+            return (None, None)
+        if hasattr(input, "read"):  # Check if the input is a file.
+            return (input, None)  # Assume the file is in the right encoding.
+
+        # Popen in Python 2.5 and before does not automatically encode unicode objects.
+        # http://bugs.python.org/issue5290
+        # See https://bugs.webkit.org/show_bug.cgi?id=37528
+        # for an example of a regresion caused by passing a unicode string directly.
+        # FIXME: We may need to encode differently on different platforms.
+        if isinstance(input, unicode):
+            input = input.encode(self._child_process_encoding())
+        return (self.PIPE, input)
+
+    def _command_for_printing(self, args):
+        """Returns a print-ready string representing command args.
+        The string should be copy/paste ready for execution in a shell."""
+        escaped_args = []
+        for arg in args:
+            if isinstance(arg, unicode):
+                # Escape any non-ascii characters for easy copy/paste
+                arg = arg.encode("unicode_escape")
+            # FIXME: Do we need to fix quotes here?
+            escaped_args.append(arg)
+        return " ".join(escaped_args)
+
+    # FIXME: run_and_throw_if_fail should be merged into this method.
+    def run_command(self,
+                    args,
+                    cwd=None,
+                    env=None,
+                    input=None,
+                    error_handler=None,
+                    return_exit_code=False,
+                    return_stderr=True,
+                    decode_output=True):
+        """Popen wrapper for convenience and to work around python bugs."""
+        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())
+        args = map(self._encode_argument_if_needed, args)
+
+        stdin, string_to_communicate = self._compute_stdin(input)
+        stderr = self.STDOUT if return_stderr else None
+
+        process = self.popen(args,
+                             stdin=stdin,
+                             stdout=self.PIPE,
+                             stderr=stderr,
+                             cwd=cwd,
+                             env=env,
+                             close_fds=self._should_close_fds())
+        output = process.communicate(string_to_communicate)[0]
+
+        # run_command automatically decodes to unicode() unless explicitly told not to.
+        if decode_output:
+            output = output.decode(self._child_process_encoding())
+
+        # wait() is not threadsafe and can throw OSError due to:
+        # http://bugs.python.org/issue1731717
+        exit_code = process.wait()
+
+        _log.debug('"%s" took %.2fs' % (self._command_for_printing(args), time.time() - start_time))
+
+        if return_exit_code:
+            return exit_code
+
+        if exit_code:
+            script_error = ScriptError(script_args=args,
+                                       exit_code=exit_code,
+                                       output=output,
+                                       cwd=cwd)
+            (error_handler or self.default_error_handler)(script_error)
+        return output
+
+    def _child_process_encoding(self):
+        # Win32 Python 2.x uses CreateProcessA rather than CreateProcessW
+        # to launch subprocesses, so we have to encode arguments using the
+        # current code page.
+        if sys.platform == 'win32' and sys.version < '3':
+            return 'mbcs'
+        # All other platforms use UTF-8.
+        # FIXME: Using UTF-8 on Cygwin will confuse Windows-native commands
+        # which will expect arguments to be encoded using the current code
+        # page.
+        return 'utf-8'
+
+    def _should_encode_child_process_arguments(self):
+        # Cygwin's Python's os.execv doesn't support unicode command
+        # arguments, and neither does Cygwin's execv itself.
+        if sys.platform == 'cygwin':
+            return True
+
+        # Win32 Python 2.x uses CreateProcessA rather than CreateProcessW
+        # to launch subprocesses, so we have to encode arguments using the
+        # current code page.
+        if sys.platform == 'win32' and sys.version < '3':
+            return True
+
+        return False
+
+    def _encode_argument_if_needed(self, argument):
+        if not self._should_encode_child_process_arguments():
+            return argument
+        return argument.encode(self._child_process_encoding())
+
+    def popen(self, *args, **kwargs):
+        return subprocess.Popen(*args, **kwargs)
+
+    def run_in_parallel(self, command_lines_and_cwds, processes=None):
+        """Runs a list of (cmd_line list, cwd string) tuples in parallel and returns a list of (retcode, stdout, stderr) tuples."""
+        assert len(command_lines_and_cwds)
+
+        if sys.platform in ('cygwin', 'win32'):
+            return map(_run_command_thunk, command_lines_and_cwds)
+        pool = multiprocessing.Pool(processes=processes)
+        results = pool.map(_run_command_thunk, command_lines_and_cwds)
+        pool.close()
+        pool.join()
+        return results
+
+
+def _run_command_thunk(cmd_line_and_cwd):
+    # Note that this needs to be a bare module (and hence Picklable) method to work with multiprocessing.Pool.
+    (cmd_line, cwd) = cmd_line_and_cwd
+    proc = subprocess.Popen(cmd_line, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    stdout, stderr = proc.communicate()
+    return (proc.returncode, stdout, stderr)
diff --git a/Tools/Scripts/webkitpy/common/system/executive_mock.py b/Tools/Scripts/webkitpy/common/system/executive_mock.py
new file mode 100644
index 0000000..c261353
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/executive_mock.py
@@ -0,0 +1,181 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import StringIO
+
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.common.system.executive import ScriptError
+
+
+class MockProcess(object):
+    def __init__(self, stdout='MOCK STDOUT\n', stderr=''):
+        self.pid = 42
+        self.stdout = StringIO.StringIO(stdout)
+        self.stderr = StringIO.StringIO(stderr)
+        self.stdin = StringIO.StringIO()
+        self.returncode = 0
+
+    def wait(self):
+        return
+
+# FIXME: This should be unified with MockExecutive2
+class MockExecutive(object):
+    PIPE = "MOCK PIPE"
+    STDOUT = "MOCK STDOUT"
+
+    @staticmethod
+    def ignore_error(error):
+        pass
+
+    def __init__(self, should_log=False, should_throw=False, should_throw_when_run=None):
+        self._should_log = should_log
+        self._should_throw = should_throw
+        self._should_throw_when_run = should_throw_when_run or set()
+        # FIXME: Once executive wraps os.getpid() we can just use a static pid for "this" process.
+        self._running_pids = {'test-webkitpy': os.getpid()}
+        self._proc = None
+        self.calls = []
+
+    def check_running_pid(self, pid):
+        return pid in self._running_pids.values()
+
+    def running_pids(self, process_name_filter):
+        running_pids = []
+        for process_name, process_pid in self._running_pids.iteritems():
+            if process_name_filter(process_name):
+                running_pids.append(process_pid)
+
+        log("MOCK running_pids: %s" % running_pids)
+        return running_pids
+
+    def run_and_throw_if_fail(self, args, quiet=False, cwd=None, env=None):
+        if self._should_log:
+            env_string = ""
+            if env:
+                env_string = ", env=%s" % env
+            log("MOCK run_and_throw_if_fail: %s, cwd=%s%s" % (args, cwd, env_string))
+        if self._should_throw_when_run.intersection(args):
+            raise ScriptError("Exception for %s" % args, output="MOCK command output")
+        return "MOCK output of child process"
+
+    def run_command(self,
+                    args,
+                    cwd=None,
+                    input=None,
+                    error_handler=None,
+                    return_exit_code=False,
+                    return_stderr=True,
+                    decode_output=False,
+                    env=None):
+
+        self.calls.append(args)
+
+        assert(isinstance(args, list) or isinstance(args, tuple))
+        if self._should_log:
+            env_string = ""
+            if env:
+                env_string = ", env=%s" % env
+            input_string = ""
+            if input:
+                input_string = ", input=%s" % input
+            log("MOCK run_command: %s, cwd=%s%s%s" % (args, cwd, env_string, input_string))
+        output = "MOCK output of child process"
+        if self._should_throw:
+            raise ScriptError("MOCK ScriptError", output=output)
+        return output
+
+    def cpu_count(self):
+        return 2
+
+    def kill_all(self, process_name):
+        pass
+
+    def kill_process(self, pid):
+        pass
+
+    def popen(self, args, cwd=None, env=None, **kwargs):
+        self.calls.append(args)
+        if self._should_log:
+            cwd_string = ""
+            if cwd:
+                cwd_string = ", cwd=%s" % cwd
+            env_string = ""
+            if env:
+                env_string = ", env=%s" % env
+            log("MOCK popen: %s%s%s" % (args, cwd_string, env_string))
+        if not self._proc:
+            self._proc = MockProcess()
+        return self._proc
+
+    def run_in_parallel(self, commands):
+        num_previous_calls = len(self.calls)
+        command_outputs = []
+        for cmd_line, cwd in commands:
+            command_outputs.append([0, self.run_command(cmd_line, cwd=cwd), ''])
+
+        new_calls = self.calls[num_previous_calls:]
+        self.calls = self.calls[:num_previous_calls]
+        self.calls.append(new_calls)
+        return command_outputs
+
+
+class MockExecutive2(MockExecutive):
+    """MockExecutive2 is like MockExecutive except it doesn't log anything."""
+
+    def __init__(self, output='', exit_code=0, exception=None, run_command_fn=None, stderr=''):
+        self._output = output
+        self._stderr = stderr
+        self._exit_code = exit_code
+        self._exception = exception
+        self._run_command_fn = run_command_fn
+        self.calls = []
+
+    def run_command(self,
+                    args,
+                    cwd=None,
+                    input=None,
+                    error_handler=None,
+                    return_exit_code=False,
+                    return_stderr=True,
+                    decode_output=False,
+                    env=None):
+        self.calls.append(args)
+        assert(isinstance(args, list) or isinstance(args, tuple))
+        if self._exception:
+            raise self._exception
+        if self._run_command_fn:
+            return self._run_command_fn(args)
+        if return_exit_code:
+            return self._exit_code
+        if self._exit_code and error_handler:
+            script_error = ScriptError(script_args=args, exit_code=self._exit_code, output=self._output)
+            error_handler(script_error)
+        if return_stderr:
+            return self._output + self._stderr
+        return self._output
diff --git a/Tools/Scripts/webkitpy/common/system/executive_unittest.py b/Tools/Scripts/webkitpy/common/system/executive_unittest.py
new file mode 100644
index 0000000..57c5573
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/executive_unittest.py
@@ -0,0 +1,257 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# Copyright (C) 2009 Daniel Bates (dbates@intudata.com). All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import errno
+import signal
+import subprocess
+import sys
+import time
+import unittest
+
+# Since we execute this script directly as part of the unit tests, we need to ensure
+# that Tools/Scripts is in sys.path for the next imports to work correctly.
+script_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
+if script_dir not in sys.path:
+    sys.path.append(script_dir)
+
+from webkitpy.common.system.executive import Executive, ScriptError
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+
+
+class ScriptErrorTest(unittest.TestCase):
+    def test_string_from_args(self):
+        error = ScriptError()
+        self.assertEquals(error._string_from_args(None), 'None')
+        self.assertEquals(error._string_from_args([]), '[]')
+        self.assertEquals(error._string_from_args(map(str, range(30))), "['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17'...")
+
+    def test_message_with_output(self):
+        error = ScriptError('My custom message!', '', -1)
+        self.assertEquals(error.message_with_output(), 'My custom message!')
+        error = ScriptError('My custom message!', '', -1, 'My output.')
+        self.assertEquals(error.message_with_output(), 'My custom message!\n\nMy output.')
+        error = ScriptError('', 'my_command!', -1, 'My output.', '/Users/username/blah')
+        self.assertEquals(error.message_with_output(), 'Failed to run "my_command!" exit_code: -1 cwd: /Users/username/blah\n\nMy output.')
+        error = ScriptError('', 'my_command!', -1, 'ab' + '1' * 499)
+        self.assertEquals(error.message_with_output(), 'Failed to run "my_command!" exit_code: -1\n\nLast 500 characters of output:\nb' + '1' * 499)
+
+def never_ending_command():
+    """Arguments for a command that will never end (useful for testing process
+    killing). It should be a process that is unlikely to already be running
+    because all instances will be killed."""
+    if sys.platform == 'win32':
+        return ['wmic']
+    return ['yes']
+
+
+def command_line(cmd, *args):
+    return [sys.executable, __file__, '--' + cmd] + list(args)
+
+
+class ExecutiveTest(unittest.TestCase):
+    def assert_interpreter_for_content(self, intepreter, content):
+        fs = MockFileSystem()
+
+        tempfile, temp_name = fs.open_binary_tempfile('')
+        tempfile.write(content)
+        tempfile.close()
+        file_interpreter = Executive.interpreter_for_script(temp_name, fs)
+
+        self.assertEqual(file_interpreter, intepreter)
+
+    def test_interpreter_for_script(self):
+        self.assert_interpreter_for_content(None, '')
+        self.assert_interpreter_for_content(None, 'abcd\nefgh\nijklm')
+        self.assert_interpreter_for_content(None, '##/usr/bin/perl')
+        self.assert_interpreter_for_content('perl', '#!/usr/bin/env perl')
+        self.assert_interpreter_for_content('perl', '#!/usr/bin/env perl\nfirst\nsecond')
+        self.assert_interpreter_for_content('perl', '#!/usr/bin/perl')
+        self.assert_interpreter_for_content('perl', '#!/usr/bin/perl -w')
+        self.assert_interpreter_for_content(sys.executable, '#!/usr/bin/env python')
+        self.assert_interpreter_for_content(sys.executable, '#!/usr/bin/env python\nfirst\nsecond')
+        self.assert_interpreter_for_content(sys.executable, '#!/usr/bin/python')
+        self.assert_interpreter_for_content('ruby', '#!/usr/bin/env ruby')
+        self.assert_interpreter_for_content('ruby', '#!/usr/bin/env ruby\nfirst\nsecond')
+        self.assert_interpreter_for_content('ruby', '#!/usr/bin/ruby')
+
+    def test_run_command_with_bad_command(self):
+        def run_bad_command():
+            Executive().run_command(["foo_bar_command_blah"], error_handler=Executive.ignore_error, return_exit_code=True)
+        self.failUnlessRaises(OSError, run_bad_command)
+
+    def test_run_command_args_type(self):
+        executive = Executive()
+        self.assertRaises(AssertionError, executive.run_command, "echo")
+        self.assertRaises(AssertionError, executive.run_command, u"echo")
+        executive.run_command(command_line('echo', 'foo'))
+        executive.run_command(tuple(command_line('echo', 'foo')))
+
+    def test_run_command_with_unicode(self):
+        """Validate that it is safe to pass unicode() objects
+        to Executive.run* methods, and they will return unicode()
+        objects by default unless decode_output=False"""
+        unicode_tor_input = u"WebKit \u2661 Tor Arne Vestb\u00F8!"
+        if sys.platform == 'win32':
+            encoding = 'mbcs'
+        else:
+            encoding = 'utf-8'
+        encoded_tor = unicode_tor_input.encode(encoding)
+        # On Windows, we expect the unicode->mbcs->unicode roundtrip to be
+        # lossy. On other platforms, we expect a lossless roundtrip.
+        if sys.platform == 'win32':
+            unicode_tor_output = encoded_tor.decode(encoding)
+        else:
+            unicode_tor_output = unicode_tor_input
+
+        executive = Executive()
+
+        output = executive.run_command(command_line('cat'), input=unicode_tor_input)
+        self.assertEquals(output, unicode_tor_output)
+
+        output = executive.run_command(command_line('echo', unicode_tor_input))
+        self.assertEquals(output, unicode_tor_output)
+
+        output = executive.run_command(command_line('echo', unicode_tor_input), decode_output=False)
+        self.assertEquals(output, encoded_tor)
+
+        # Make sure that str() input also works.
+        output = executive.run_command(command_line('cat'), input=encoded_tor, decode_output=False)
+        self.assertEquals(output, encoded_tor)
+
+        # FIXME: We should only have one run* method to test
+        output = executive.run_and_throw_if_fail(command_line('echo', unicode_tor_input), quiet=True)
+        self.assertEquals(output, unicode_tor_output)
+
+        output = executive.run_and_throw_if_fail(command_line('echo', unicode_tor_input), quiet=True, decode_output=False)
+        self.assertEquals(output, encoded_tor)
+
+    def serial_test_kill_process(self):
+        executive = Executive()
+        process = subprocess.Popen(never_ending_command(), stdout=subprocess.PIPE)
+        self.assertEqual(process.poll(), None)  # Process is running
+        executive.kill_process(process.pid)
+        # Note: Can't use a ternary since signal.SIGKILL is undefined for sys.platform == "win32"
+        if sys.platform == "win32":
+            # FIXME: https://bugs.webkit.org/show_bug.cgi?id=54790
+            # We seem to get either 0 or 1 here for some reason.
+            self.assertTrue(process.wait() in (0, 1))
+        elif sys.platform == "cygwin":
+            # FIXME: https://bugs.webkit.org/show_bug.cgi?id=98196
+            # cygwin seems to give us either SIGABRT or SIGKILL
+            self.assertTrue(process.wait() in (-signal.SIGABRT, -signal.SIGKILL))
+        else:
+            expected_exit_code = -signal.SIGKILL
+            self.assertEqual(process.wait(), expected_exit_code)
+
+        # Killing again should fail silently.
+        executive.kill_process(process.pid)
+
+    def serial_test_kill_all(self):
+        executive = Executive()
+        process = subprocess.Popen(never_ending_command(), stdout=subprocess.PIPE)
+        self.assertEqual(process.poll(), None)  # Process is running
+        executive.kill_all(never_ending_command()[0])
+        # Note: Can't use a ternary since signal.SIGTERM is undefined for sys.platform == "win32"
+        if sys.platform == "cygwin":
+            expected_exit_code = 0  # os.kill results in exit(0) for this process.
+            self.assertEqual(process.wait(), expected_exit_code)
+        elif sys.platform == "win32":
+            # FIXME: https://bugs.webkit.org/show_bug.cgi?id=54790
+            # We seem to get either 0 or 1 here for some reason.
+            self.assertTrue(process.wait() in (0, 1))
+        else:
+            expected_exit_code = -signal.SIGTERM
+            self.assertEqual(process.wait(), expected_exit_code)
+        # Killing again should fail silently.
+        executive.kill_all(never_ending_command()[0])
+
+    def _assert_windows_image_name(self, name, expected_windows_name):
+        executive = Executive()
+        windows_name = executive._windows_image_name(name)
+        self.assertEqual(windows_name, expected_windows_name)
+
+    def test_windows_image_name(self):
+        self._assert_windows_image_name("foo", "foo.exe")
+        self._assert_windows_image_name("foo.exe", "foo.exe")
+        self._assert_windows_image_name("foo.com", "foo.com")
+        # If the name looks like an extension, even if it isn't
+        # supposed to, we have no choice but to return the original name.
+        self._assert_windows_image_name("foo.baz", "foo.baz")
+        self._assert_windows_image_name("foo.baz.exe", "foo.baz.exe")
+
+    def serial_test_check_running_pid(self):
+        executive = Executive()
+        self.assertTrue(executive.check_running_pid(os.getpid()))
+        # Maximum pid number on Linux is 32768 by default
+        self.assertFalse(executive.check_running_pid(100000))
+
+    def serial_test_running_pids(self):
+        if sys.platform in ("win32", "cygwin"):
+            return  # This function isn't implemented on Windows yet.
+
+        executive = Executive()
+        pids = executive.running_pids()
+        self.assertTrue(os.getpid() in pids)
+
+    def serial_test_run_in_parallel(self):
+        # We run this test serially to avoid overloading the machine and throwing off the timing.
+
+        if sys.platform in ("win32", "cygwin"):
+            return  # This function isn't implemented properly on windows yet.
+        import multiprocessing
+
+        NUM_PROCESSES = 4
+        DELAY_SECS = 0.25
+        cmd_line = [sys.executable, '-c', 'import time; time.sleep(%f); print "hello"' % DELAY_SECS]
+        cwd = os.getcwd()
+        commands = [tuple([cmd_line, cwd])] * NUM_PROCESSES
+        start = time.time()
+        command_outputs = Executive().run_in_parallel(commands, processes=NUM_PROCESSES)
+        done = time.time()
+        self.assertTrue(done - start < NUM_PROCESSES * DELAY_SECS)
+        self.assertEquals([output[1] for output in command_outputs], ["hello\n"] * NUM_PROCESSES)
+        self.assertEquals([],  multiprocessing.active_children())
+
+    def test_run_in_parallel_assert_nonempty(self):
+        self.assertRaises(AssertionError, Executive().run_in_parallel, [])
+
+
+def main(platform, stdin, stdout, cmd, args):
+    if platform == 'win32' and hasattr(stdout, 'fileno'):
+        import msvcrt
+        msvcrt.setmode(stdout.fileno(), os.O_BINARY)
+    if cmd == '--cat':
+        stdout.write(stdin.read())
+    elif cmd == '--echo':
+        stdout.write(' '.join(args))
+    return 0
+
+if __name__ == '__main__' and len(sys.argv) > 1 and sys.argv[1] in ('--cat', '--echo'):
+    sys.exit(main(sys.platform, sys.stdin, sys.stdout, sys.argv[1], sys.argv[2:]))
diff --git a/Tools/Scripts/webkitpy/common/system/file_lock.py b/Tools/Scripts/webkitpy/common/system/file_lock.py
new file mode 100644
index 0000000..c542777
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/file_lock.py
@@ -0,0 +1,85 @@
+#!/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.
+
+"""This class helps to lock files exclusively across processes."""
+
+import logging
+import os
+import sys
+import time
+
+
+_log = logging.getLogger(__name__)
+
+
+class FileLock(object):
+
+    def __init__(self, lock_file_path, max_wait_time_sec=20):
+        self._lock_file_path = lock_file_path
+        self._lock_file_descriptor = None
+        self._max_wait_time_sec = max_wait_time_sec
+
+    def _create_lock(self):
+        if sys.platform == 'win32':
+            import msvcrt
+            msvcrt.locking(self._lock_file_descriptor, msvcrt.LK_NBLCK, 32)
+        else:
+            import fcntl
+            fcntl.flock(self._lock_file_descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
+
+    def _remove_lock(self):
+        if sys.platform == 'win32':
+            import msvcrt
+            msvcrt.locking(self._lock_file_descriptor, msvcrt.LK_UNLCK, 32)
+        else:
+            import fcntl
+            fcntl.flock(self._lock_file_descriptor, fcntl.LOCK_UN)
+
+    def acquire_lock(self):
+        self._lock_file_descriptor = os.open(self._lock_file_path, os.O_TRUNC | os.O_CREAT)
+        start_time = time.time()
+        while True:
+            try:
+                self._create_lock()
+                return True
+            except IOError:
+                if time.time() - start_time > self._max_wait_time_sec:
+                    _log.debug("File locking failed: %s" % str(sys.exc_info()))
+                    os.close(self._lock_file_descriptor)
+                    self._lock_file_descriptor = None
+                    return False
+                # There's no compelling reason to spin hard here, so sleep for a bit.
+                time.sleep(0.01)
+
+    def release_lock(self):
+        try:
+            if self._lock_file_descriptor:
+                self._remove_lock()
+                os.close(self._lock_file_descriptor)
+                self._lock_file_descriptor = None
+            os.unlink(self._lock_file_path)
+        except (IOError, OSError):
+            _log.debug("Warning in release lock: %s" % str(sys.exc_info()))
diff --git a/Tools/Scripts/webkitpy/common/system/file_lock_integrationtest.py b/Tools/Scripts/webkitpy/common/system/file_lock_integrationtest.py
new file mode 100644
index 0000000..5cd27d1
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/file_lock_integrationtest.py
@@ -0,0 +1,61 @@
+#!/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 os
+import tempfile
+import unittest
+
+from webkitpy.common.system.file_lock import FileLock
+
+
+class FileLockTest(unittest.TestCase):
+
+    def setUp(self):
+        self._lock_name = "TestWebKit" + str(os.getpid()) + ".lock"
+        self._lock_path = os.path.join(tempfile.gettempdir(), self._lock_name)
+        self._file_lock1 = FileLock(self._lock_path, 0.1)
+        self._file_lock2 = FileLock(self._lock_path, 0.1)
+
+    def tearDown(self):
+        self._file_lock1.release_lock()
+        self._file_lock2.release_lock()
+
+    def test_lock_lifecycle(self):
+        # Create the lock.
+        self._file_lock1.acquire_lock()
+        self.assertTrue(os.path.exists(self._lock_path))
+
+        # Try to lock again.
+        self.assertFalse(self._file_lock2.acquire_lock())
+
+        # Release the lock.
+        self._file_lock1.release_lock()
+        self.assertFalse(os.path.exists(self._lock_path))
+
+    def test_stuck_lock(self):
+        open(self._lock_path, 'w').close()
+        self._file_lock1.acquire_lock()
+        self._file_lock1.release_lock()
diff --git a/Tools/Scripts/webkitpy/common/system/file_lock_mock.py b/Tools/Scripts/webkitpy/common/system/file_lock_mock.py
new file mode 100644
index 0000000..e2c1d5c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/file_lock_mock.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 Google Inc. All rights reserved.
+#
+# 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.
+
+
+class MockFileLock(object):
+    def __init__(self, lock_file_path, max_wait_time_sec=20):
+        pass
+
+    def acquire_lock(self):
+        pass
+
+    def release_lock(self):
+        pass
diff --git a/Tools/Scripts/webkitpy/common/system/fileset.py b/Tools/Scripts/webkitpy/common/system/fileset.py
new file mode 100644
index 0000000..57e9a28
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/fileset.py
@@ -0,0 +1,64 @@
+# 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:
+#
+# 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.
+
+from webkitpy.common.system.filesystem import FileSystem
+
+
+class FileSetFileHandle(object):
+    """Points to a file that resides in a file set"""
+    def __init__(self, fileset, filename, filesystem=None):
+        self._filename = filename
+        self._fileset = fileset
+        self._contents = None
+        self._filesystem = filesystem or FileSystem()
+
+    def __str__(self):
+        return "%s:%s" % (self._fileset, self._filename)
+
+    def close(self):
+        pass
+
+    def contents(self):
+        if self._contents is None:
+            self._contents = self._fileset.read(self._filename)
+        return self._contents
+
+    def save_to(self, path, filename=None):
+        if filename is None:
+            self._fileset.extract(self._filename, path)
+            return
+        with self._filesystem.mkdtemp() as temp_dir:
+            self._fileset.extract(self._filename, temp_dir)
+
+            src = self._filesystem.join(temp_dir, self._filename)
+            dest = self._filesystem.join(path, filename)
+            self._filesystem.copyfile(src, dest)
+
+    def delete(self):
+        self._fileset.delete(self._filename)
+
+    def name(self):
+        return self._filename
+
+    def splitext(self):
+        return self._filesystem.splitext(self.name())
diff --git a/Tools/Scripts/webkitpy/common/system/filesystem.py b/Tools/Scripts/webkitpy/common/system/filesystem.py
new file mode 100644
index 0000000..3786c6f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/filesystem.py
@@ -0,0 +1,269 @@
+# 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.
+
+"""Wrapper object for the file system / source tree."""
+
+import codecs
+import errno
+import exceptions
+import glob
+import hashlib
+import os
+import shutil
+import sys
+import tempfile
+import time
+
+class FileSystem(object):
+    """FileSystem interface for webkitpy.
+
+    Unless otherwise noted, all paths are allowed to be either absolute
+    or relative."""
+    sep = os.sep
+    pardir = os.pardir
+
+    def abspath(self, path):
+        return os.path.abspath(path)
+
+    def realpath(self, path):
+        return os.path.realpath(path)
+
+    def path_to_module(self, module_name):
+        """A wrapper for all calls to __file__ to allow easy unit testing."""
+        # FIXME: This is the only use of sys in this file. It's possible this function should move elsewhere.
+        return sys.modules[module_name].__file__  # __file__ is always an absolute path.
+
+    def expanduser(self, path):
+        return os.path.expanduser(path)
+
+    def basename(self, path):
+        return os.path.basename(path)
+
+    def chdir(self, path):
+        return os.chdir(path)
+
+    def copyfile(self, source, destination):
+        shutil.copyfile(source, destination)
+
+    def dirname(self, path):
+        return os.path.dirname(path)
+
+    def exists(self, path):
+        return os.path.exists(path)
+
+    def files_under(self, path, dirs_to_skip=[], file_filter=None):
+        """Return the list of all files under the given path in topdown order.
+
+        Args:
+            dirs_to_skip: a list of directories to skip over during the
+                traversal (e.g., .svn, resources, etc.)
+            file_filter: if not None, the filter will be invoked
+                with the filesystem object and the dirname and basename of
+                each file found. The file is included in the result if the
+                callback returns True.
+        """
+        def filter_all(fs, dirpath, basename):
+            return True
+
+        file_filter = file_filter or filter_all
+        files = []
+        if self.isfile(path):
+            if file_filter(self, self.dirname(path), self.basename(path)):
+                files.append(path)
+            return files
+
+        if self.basename(path) in dirs_to_skip:
+            return []
+
+        for (dirpath, dirnames, filenames) in os.walk(path):
+            for d in dirs_to_skip:
+                if d in dirnames:
+                    dirnames.remove(d)
+
+            for filename in filenames:
+                if file_filter(self, dirpath, filename):
+                    files.append(self.join(dirpath, filename))
+        return files
+
+    def getcwd(self):
+        return os.getcwd()
+
+    def glob(self, path):
+        return glob.glob(path)
+
+    def isabs(self, path):
+        return os.path.isabs(path)
+
+    def isfile(self, path):
+        return os.path.isfile(path)
+
+    def isdir(self, path):
+        return os.path.isdir(path)
+
+    def join(self, *comps):
+        return os.path.join(*comps)
+
+    def listdir(self, path):
+        return os.listdir(path)
+
+    def mkdtemp(self, **kwargs):
+        """Create and return a uniquely named directory.
+
+        This is like tempfile.mkdtemp, but if used in a with statement
+        the directory will self-delete at the end of the block (if the
+        directory is empty; non-empty directories raise errors). The
+        directory can be safely deleted inside the block as well, if so
+        desired.
+
+        Note that the object returned is not a string and does not support all of the string
+        methods. If you need a string, coerce the object to a string and go from there.
+        """
+        class TemporaryDirectory(object):
+            def __init__(self, **kwargs):
+                self._kwargs = kwargs
+                self._directory_path = tempfile.mkdtemp(**self._kwargs)
+
+            def __str__(self):
+                return self._directory_path
+
+            def __enter__(self):
+                return self._directory_path
+
+            def __exit__(self, type, value, traceback):
+                # Only self-delete if necessary.
+
+                # FIXME: Should we delete non-empty directories?
+                if os.path.exists(self._directory_path):
+                    os.rmdir(self._directory_path)
+
+        return TemporaryDirectory(**kwargs)
+
+    def maybe_make_directory(self, *path):
+        """Create the specified directory if it doesn't already exist."""
+        try:
+            os.makedirs(self.join(*path))
+        except OSError, e:
+            if e.errno != errno.EEXIST:
+                raise
+
+    def move(self, source, destination):
+        shutil.move(source, destination)
+
+    def mtime(self, path):
+        return os.stat(path).st_mtime
+
+    def normpath(self, path):
+        return os.path.normpath(path)
+
+    def open_binary_tempfile(self, suffix):
+        """Create, open, and return a binary temp file. Returns a tuple of the file and the name."""
+        temp_fd, temp_name = tempfile.mkstemp(suffix)
+        f = os.fdopen(temp_fd, 'wb')
+        return f, temp_name
+
+    def open_binary_file_for_reading(self, path):
+        return codecs.open(path, 'rb')
+
+    def read_binary_file(self, path):
+        """Return the contents of the file at the given path as a byte string."""
+        with file(path, 'rb') as f:
+            return f.read()
+
+    def write_binary_file(self, path, contents):
+        with file(path, 'wb') as f:
+            f.write(contents)
+
+    def open_text_file_for_reading(self, path):
+        # Note: There appears to be an issue with the returned file objects
+        # not being seekable. See http://stackoverflow.com/questions/1510188/can-seek-and-tell-work-with-utf-8-encoded-documents-in-python .
+        return codecs.open(path, 'r', 'utf8')
+
+    def open_text_file_for_writing(self, path):
+        return codecs.open(path, 'w', 'utf8')
+
+    def read_text_file(self, path):
+        """Return the contents of the file at the given path as a Unicode string.
+
+        The file is read assuming it is a UTF-8 encoded file with no BOM."""
+        with codecs.open(path, 'r', 'utf8') as f:
+            return f.read()
+
+    def write_text_file(self, path, contents):
+        """Write the contents to the file at the given location.
+
+        The file is written encoded as UTF-8 with no BOM."""
+        with codecs.open(path, 'w', 'utf8') as f:
+            f.write(contents)
+
+    def sha1(self, path):
+        contents = self.read_binary_file(path)
+        return hashlib.sha1(contents).hexdigest()
+
+    def relpath(self, path, start='.'):
+        return os.path.relpath(path, start)
+
+    class _WindowsError(exceptions.OSError):
+        """Fake exception for Linux and Mac."""
+        pass
+
+    def remove(self, path, osremove=os.remove):
+        """On Windows, if a process was recently killed and it held on to a
+        file, the OS will hold on to the file for a short while.  This makes
+        attempts to delete the file fail.  To work around that, this method
+        will retry for a few seconds until Windows is done with the file."""
+        try:
+            exceptions.WindowsError
+        except AttributeError:
+            exceptions.WindowsError = FileSystem._WindowsError
+
+        retry_timeout_sec = 3.0
+        sleep_interval = 0.1
+        while True:
+            try:
+                osremove(path)
+                return True
+            except exceptions.WindowsError, e:
+                time.sleep(sleep_interval)
+                retry_timeout_sec -= sleep_interval
+                if retry_timeout_sec < 0:
+                    raise e
+
+    def rmtree(self, path):
+        """Delete the directory rooted at path, whether empty or not."""
+        shutil.rmtree(path, ignore_errors=True)
+
+    def copytree(self, source, destination):
+        shutil.copytree(source, destination)
+
+    def split(self, path):
+        """Return (dirname, basename + '.' + ext)"""
+        return os.path.split(path)
+
+    def splitext(self, path):
+        """Return (dirname + os.sep + basename, '.' + ext)"""
+        return os.path.splitext(path)
diff --git a/Tools/Scripts/webkitpy/common/system/filesystem_mock.py b/Tools/Scripts/webkitpy/common/system/filesystem_mock.py
new file mode 100644
index 0000000..d87fe1b
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/filesystem_mock.py
@@ -0,0 +1,474 @@
+# Copyright (C) 2009 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 StringIO
+import errno
+import hashlib
+import os
+import re
+
+from webkitpy.common.system import path
+
+
+class MockFileSystem(object):
+    sep = '/'
+    pardir = '..'
+
+    def __init__(self, files=None, dirs=None, cwd='/'):
+        """Initializes a "mock" filesystem that can be used to completely
+        stub out a filesystem.
+
+        Args:
+            files: a dict of filenames -> file contents. A file contents
+                value of None is used to indicate that the file should
+                not exist.
+        """
+        self.files = files or {}
+        self.written_files = {}
+        self.last_tmpdir = None
+        self.current_tmpno = 0
+        self.cwd = cwd
+        self.dirs = set(dirs or [])
+        self.dirs.add(cwd)
+        for f in self.files:
+            d = self.dirname(f)
+            while not d in self.dirs:
+                self.dirs.add(d)
+                d = self.dirname(d)
+
+    def clear_written_files(self):
+        # This function can be used to track what is written between steps in a test.
+        self.written_files = {}
+
+    def _raise_not_found(self, path):
+        raise IOError(errno.ENOENT, path, os.strerror(errno.ENOENT))
+
+    def _split(self, path):
+        # This is not quite a full implementation of os.path.split
+        # http://docs.python.org/library/os.path.html#os.path.split
+        if self.sep in path:
+            return path.rsplit(self.sep, 1)
+        return ('', path)
+
+    def abspath(self, path):
+        if os.path.isabs(path):
+            return self.normpath(path)
+        return self.abspath(self.join(self.cwd, path))
+
+    def realpath(self, path):
+        return self.abspath(path)
+
+    def basename(self, path):
+        return self._split(path)[1]
+
+    def expanduser(self, path):
+        if path[0] != "~":
+            return path
+        parts = path.split(self.sep, 1)
+        home_directory = self.sep + "Users" + self.sep + "mock"
+        if len(parts) == 1:
+            return home_directory
+        return home_directory + self.sep + parts[1]
+
+    def path_to_module(self, module_name):
+        return "/mock-checkout/Tools/Scripts/" + module_name.replace('.', '/') + ".py"
+
+    def chdir(self, path):
+        path = self.normpath(path)
+        if not self.isdir(path):
+            raise OSError(errno.ENOENT, path, os.strerror(errno.ENOENT))
+        self.cwd = path
+
+    def copyfile(self, source, destination):
+        if not self.exists(source):
+            self._raise_not_found(source)
+        if self.isdir(source):
+            raise IOError(errno.EISDIR, source, os.strerror(errno.ISDIR))
+        if self.isdir(destination):
+            raise IOError(errno.EISDIR, destination, os.strerror(errno.ISDIR))
+        if not self.exists(self.dirname(destination)):
+            raise IOError(errno.ENOENT, destination, os.strerror(errno.ENOENT))
+
+        self.files[destination] = self.files[source]
+        self.written_files[destination] = self.files[source]
+
+    def dirname(self, path):
+        return self._split(path)[0]
+
+    def exists(self, path):
+        return self.isfile(path) or self.isdir(path)
+
+    def files_under(self, path, dirs_to_skip=[], file_filter=None):
+        def filter_all(fs, dirpath, basename):
+            return True
+
+        file_filter = file_filter or filter_all
+        files = []
+        if self.isfile(path):
+            if file_filter(self, self.dirname(path), self.basename(path)) and self.files[path] is not None:
+                files.append(path)
+            return files
+
+        if self.basename(path) in dirs_to_skip:
+            return []
+
+        if not path.endswith(self.sep):
+            path += self.sep
+
+        dir_substrings = [self.sep + d + self.sep for d in dirs_to_skip]
+        for filename in self.files:
+            if not filename.startswith(path):
+                continue
+
+            suffix = filename[len(path) - 1:]
+            if any(dir_substring in suffix for dir_substring in dir_substrings):
+                continue
+
+            dirpath, basename = self._split(filename)
+            if file_filter(self, dirpath, basename) and self.files[filename] is not None:
+                files.append(filename)
+
+        return files
+
+    def getcwd(self):
+        return self.cwd
+
+    def glob(self, glob_string):
+        # FIXME: This handles '*', but not '?', '[', or ']'.
+        glob_string = re.escape(glob_string)
+        glob_string = glob_string.replace('\\*', '[^\\/]*') + '$'
+        glob_string = glob_string.replace('\\/', '/')
+        path_filter = lambda path: re.match(glob_string, path)
+
+        # We could use fnmatch.fnmatch, but that might not do the right thing on windows.
+        existing_files = [path for path, contents in self.files.items() if contents is not None]
+        return filter(path_filter, existing_files) + filter(path_filter, self.dirs)
+
+    def isabs(self, path):
+        return path.startswith(self.sep)
+
+    def isfile(self, path):
+        return path in self.files and self.files[path] is not None
+
+    def isdir(self, path):
+        return self.normpath(path) in self.dirs
+
+    def _slow_but_correct_join(self, *comps):
+        return re.sub(re.escape(os.path.sep), self.sep, os.path.join(*comps))
+
+    def join(self, *comps):
+        # This function is called a lot, so we optimize it; there are
+        # unittests to check that we match _slow_but_correct_join(), above.
+        path = ''
+        sep = self.sep
+        for comp in comps:
+            if not comp:
+                continue
+            if comp[0] == sep:
+                path = comp
+                continue
+            if path:
+                path += sep
+            path += comp
+        if comps[-1] == '' and path:
+            path += '/'
+        path = path.replace(sep + sep, sep)
+        return path
+
+    def listdir(self, path):
+        sep = self.sep
+        if not self.isdir(path):
+            raise OSError("%s is not a directory" % path)
+
+        if not path.endswith(sep):
+            path += sep
+
+        dirs = []
+        files = []
+        for f in self.files:
+            if self.exists(f) and f.startswith(path):
+                remaining = f[len(path):]
+                if sep in remaining:
+                    dir = remaining[:remaining.index(sep)]
+                    if not dir in dirs:
+                        dirs.append(dir)
+                else:
+                    files.append(remaining)
+        return dirs + files
+
+    def mtime(self, path):
+        if self.exists(path):
+            return 0
+        self._raise_not_found(path)
+
+    def _mktemp(self, suffix='', prefix='tmp', dir=None, **kwargs):
+        if dir is None:
+            dir = self.sep + '__im_tmp'
+        curno = self.current_tmpno
+        self.current_tmpno += 1
+        self.last_tmpdir = self.join(dir, '%s_%u_%s' % (prefix, curno, suffix))
+        return self.last_tmpdir
+
+    def mkdtemp(self, **kwargs):
+        class TemporaryDirectory(object):
+            def __init__(self, fs, **kwargs):
+                self._kwargs = kwargs
+                self._filesystem = fs
+                self._directory_path = fs._mktemp(**kwargs)
+                fs.maybe_make_directory(self._directory_path)
+
+            def __str__(self):
+                return self._directory_path
+
+            def __enter__(self):
+                return self._directory_path
+
+            def __exit__(self, type, value, traceback):
+                # Only self-delete if necessary.
+
+                # FIXME: Should we delete non-empty directories?
+                if self._filesystem.exists(self._directory_path):
+                    self._filesystem.rmtree(self._directory_path)
+
+        return TemporaryDirectory(fs=self, **kwargs)
+
+    def maybe_make_directory(self, *path):
+        norm_path = self.normpath(self.join(*path))
+        while norm_path and not self.isdir(norm_path):
+            self.dirs.add(norm_path)
+            norm_path = self.dirname(norm_path)
+
+    def move(self, source, destination):
+        if self.files[source] is None:
+            self._raise_not_found(source)
+        self.files[destination] = self.files[source]
+        self.written_files[destination] = self.files[destination]
+        self.files[source] = None
+        self.written_files[source] = None
+
+    def _slow_but_correct_normpath(self, path):
+        return re.sub(re.escape(os.path.sep), self.sep, os.path.normpath(path))
+
+    def normpath(self, path):
+        # This function is called a lot, so we try to optimize the common cases
+        # instead of always calling _slow_but_correct_normpath(), above.
+        if '..' in path or '/./' in path:
+            # This doesn't happen very often; don't bother trying to optimize it.
+            return self._slow_but_correct_normpath(path)
+        if not path:
+            return '.'
+        if path == '/':
+            return path
+        if path == '/.':
+            return '/'
+        if path.endswith('/.'):
+            return path[:-2]
+        if path.endswith('/'):
+            return path[:-1]
+        return path
+
+    def open_binary_tempfile(self, suffix=''):
+        path = self._mktemp(suffix)
+        return (WritableBinaryFileObject(self, path), path)
+
+    def open_binary_file_for_reading(self, path):
+        if self.files[path] is None:
+            self._raise_not_found(path)
+        return ReadableBinaryFileObject(self, path, self.files[path])
+
+    def read_binary_file(self, path):
+        # Intentionally raises KeyError if we don't recognize the path.
+        if self.files[path] is None:
+            self._raise_not_found(path)
+        return self.files[path]
+
+    def write_binary_file(self, path, contents):
+        # FIXME: should this assert if dirname(path) doesn't exist?
+        self.maybe_make_directory(self.dirname(path))
+        self.files[path] = contents
+        self.written_files[path] = contents
+
+    def open_text_file_for_reading(self, path):
+        if self.files[path] is None:
+            self._raise_not_found(path)
+        return ReadableTextFileObject(self, path, self.files[path])
+
+    def open_text_file_for_writing(self, path):
+        return WritableTextFileObject(self, path)
+
+    def read_text_file(self, path):
+        return self.read_binary_file(path).decode('utf-8')
+
+    def write_text_file(self, path, contents):
+        return self.write_binary_file(path, contents.encode('utf-8'))
+
+    def sha1(self, path):
+        contents = self.read_binary_file(path)
+        return hashlib.sha1(contents).hexdigest()
+
+    def relpath(self, path, start='.'):
+        # Since os.path.relpath() calls os.path.normpath()
+        # (see http://docs.python.org/library/os.path.html#os.path.abspath )
+        # it also removes trailing slashes and converts forward and backward
+        # slashes to the preferred slash os.sep.
+        start = self.abspath(start)
+        path = self.abspath(path)
+
+        if not path.lower().startswith(start.lower()):
+            # path is outside the directory given by start; compute path from root
+            return '../' * start.count('/') + path
+
+        rel_path = path[len(start):]
+
+        if not rel_path:
+            # Then the paths are the same.
+            pass
+        elif rel_path[0] == self.sep:
+            # It is probably sufficient to remove just the first character
+            # since os.path.normpath() collapses separators, but we use
+            # lstrip() just to be sure.
+            rel_path = rel_path.lstrip(self.sep)
+        else:
+            # We are in the case typified by the following example:
+            # path = "/tmp/foobar", start = "/tmp/foo" -> rel_path = "bar"
+            # FIXME: We return a less-than-optimal result here.
+            return '../' * start.count('/') + path
+
+        return rel_path
+
+    def remove(self, path):
+        if self.files[path] is None:
+            self._raise_not_found(path)
+        self.files[path] = None
+        self.written_files[path] = None
+
+    def rmtree(self, path):
+        path = self.normpath(path)
+
+        for f in self.files:
+            if f.startswith(path):
+                self.files[f] = None
+
+        self.dirs = set(filter(lambda d: not d.startswith(path), self.dirs))
+
+    def copytree(self, source, destination):
+        source = self.normpath(source)
+        destination = self.normpath(destination)
+
+        for source_file in self.files:
+            if source_file.startswith(source):
+                destination_path = self.join(destination, self.relpath(source_file, source))
+                self.maybe_make_directory(self.dirname(destination_path))
+                self.files[destination_path] = self.files[source_file]
+
+    def split(self, path):
+        idx = path.rfind(self.sep)
+        if idx == -1:
+            return ('', path)
+        return (path[:idx], path[(idx + 1):])
+
+    def splitext(self, path):
+        idx = path.rfind('.')
+        if idx == -1:
+            idx = 0
+        return (path[0:idx], path[idx:])
+
+
+class WritableBinaryFileObject(object):
+    def __init__(self, fs, path):
+        self.fs = fs
+        self.path = path
+        self.closed = False
+        self.fs.files[path] = ""
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self.close()
+
+    def close(self):
+        self.closed = True
+
+    def write(self, str):
+        self.fs.files[self.path] += str
+        self.fs.written_files[self.path] = self.fs.files[self.path]
+
+
+class WritableTextFileObject(WritableBinaryFileObject):
+    def write(self, str):
+        WritableBinaryFileObject.write(self, str.encode('utf-8'))
+
+
+class ReadableBinaryFileObject(object):
+    def __init__(self, fs, path, data):
+        self.fs = fs
+        self.path = path
+        self.closed = False
+        self.data = data
+        self.offset = 0
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self.close()
+
+    def close(self):
+        self.closed = True
+
+    def read(self, bytes=None):
+        if not bytes:
+            return self.data[self.offset:]
+        start = self.offset
+        self.offset += bytes
+        return self.data[start:self.offset]
+
+
+class ReadableTextFileObject(ReadableBinaryFileObject):
+    def __init__(self, fs, path, data):
+        super(ReadableTextFileObject, self).__init__(fs, path, StringIO.StringIO(data))
+
+    def close(self):
+        self.data.close()
+        super(ReadableTextFileObject, self).close()
+
+    def read(self, bytes=-1):
+        return self.data.read(bytes)
+
+    def readline(self, length=None):
+        return self.data.readline(length)
+
+    def __iter__(self):
+        return self.data.__iter__()
+
+    def next(self):
+        return self.data.next()
+
+    def seek(self, offset, whence=os.SEEK_SET):
+        self.data.seek(offset, whence)
diff --git a/Tools/Scripts/webkitpy/common/system/filesystem_mock_unittest.py b/Tools/Scripts/webkitpy/common/system/filesystem_mock_unittest.py
new file mode 100644
index 0000000..2a6ccbf
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/filesystem_mock_unittest.py
@@ -0,0 +1,88 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import re
+import unittest
+
+
+from webkitpy.common.system import filesystem_mock
+from webkitpy.common.system import filesystem_unittest
+
+
+class MockFileSystemTest(unittest.TestCase, filesystem_unittest.GenericFileSystemTests):
+    def setUp(self):
+        self.fs = filesystem_mock.MockFileSystem()
+        self.setup_generic_test_dir()
+
+    def tearDown(self):
+        self.teardown_generic_test_dir()
+        self.fs = None
+
+    def quick_check(self, test_fn, good_fn, *tests):
+        for test in tests:
+            if hasattr(test, '__iter__'):
+                expected = good_fn(*test)
+                actual = test_fn(*test)
+            else:
+                expected = good_fn(test)
+                actual = test_fn(test)
+            self.assertEquals(expected, actual, 'given %s, expected %s, got %s' % (repr(test), repr(expected), repr(actual)))
+
+    def test_join(self):
+        self.quick_check(self.fs.join,
+                         self.fs._slow_but_correct_join,
+                         ('',),
+                         ('', 'bar'),
+                         ('foo',),
+                         ('foo/',),
+                         ('foo', ''),
+                         ('foo/', ''),
+                         ('foo', 'bar'),
+                         ('foo', '/bar'),
+                         )
+
+    def test_normpath(self):
+        self.quick_check(self.fs.normpath,
+                         self.fs._slow_but_correct_normpath,
+                         '',
+                         '/',
+                         '.',
+                         '/.',
+                         'foo',
+                         'foo/',
+                         'foo/.',
+                         'foo/bar',
+                         '/foo',
+                         'foo/../bar',
+                         'foo/../bar/baz',
+                         '../foo')
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/common/system/filesystem_unittest.py b/Tools/Scripts/webkitpy/common/system/filesystem_unittest.py
new file mode 100644
index 0000000..e6d1e42
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/filesystem_unittest.py
@@ -0,0 +1,260 @@
+# vim: set fileencoding=utf-8 :
+# 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.
+
+# NOTE: The fileencoding comment on the first line of the file is
+# important; without it, Python will choke while trying to parse the file,
+# since it includes non-ASCII characters.
+
+import os
+import stat
+import sys
+import tempfile
+import unittest
+
+from filesystem import FileSystem
+
+
+class GenericFileSystemTests(object):
+    """Tests that should pass on either a real or mock filesystem."""
+    def setup_generic_test_dir(self):
+        fs = self.fs
+        self.generic_test_dir = str(self.fs.mkdtemp())
+        self.orig_cwd = fs.getcwd()
+        fs.chdir(self.generic_test_dir)
+        fs.write_text_file('foo.txt', 'foo')
+        fs.write_text_file('foobar', 'foobar')
+        fs.maybe_make_directory('foodir')
+        fs.write_text_file(fs.join('foodir', 'baz'), 'baz')
+        fs.chdir(self.orig_cwd)
+
+    def teardown_generic_test_dir(self):
+        self.fs.rmtree(self.generic_test_dir)
+        self.fs.chdir(self.orig_cwd)
+        self.generic_test_dir = None
+
+    def test_glob__trailing_asterisk(self):
+        self.fs.chdir(self.generic_test_dir)
+        self.assertEquals(set(self.fs.glob('fo*')), set(['foo.txt', 'foobar', 'foodir']))
+
+    def test_glob__leading_asterisk(self):
+        self.fs.chdir(self.generic_test_dir)
+        self.assertEquals(set(self.fs.glob('*xt')), set(['foo.txt']))
+
+    def test_glob__middle_asterisk(self):
+        self.fs.chdir(self.generic_test_dir)
+        self.assertEquals(set(self.fs.glob('f*r')), set(['foobar', 'foodir']))
+
+    def test_glob__period_is_escaped(self):
+        self.fs.chdir(self.generic_test_dir)
+        self.assertEquals(set(self.fs.glob('foo.*')), set(['foo.txt']))
+
+class RealFileSystemTest(unittest.TestCase, GenericFileSystemTests):
+    def setUp(self):
+        self.fs = FileSystem()
+        self.setup_generic_test_dir()
+
+        self._this_dir = os.path.dirname(os.path.abspath(__file__))
+        self._missing_file = os.path.join(self._this_dir, 'missing_file.py')
+        self._this_file = os.path.join(self._this_dir, 'filesystem_unittest.py')
+
+    def tearDown(self):
+        self.teardown_generic_test_dir()
+        self.fs = None
+
+    def test_chdir(self):
+        fs = FileSystem()
+        cwd = fs.getcwd()
+        newdir = '/'
+        if sys.platform == 'win32':
+            newdir = 'c:\\'
+        fs.chdir(newdir)
+        self.assertEquals(fs.getcwd(), newdir)
+        fs.chdir(cwd)
+
+    def test_chdir__notexists(self):
+        fs = FileSystem()
+        newdir = '/dirdoesnotexist'
+        if sys.platform == 'win32':
+            newdir = 'c:\\dirdoesnotexist'
+        self.assertRaises(OSError, fs.chdir, newdir)
+
+    def test_exists__true(self):
+        fs = FileSystem()
+        self.assertTrue(fs.exists(self._this_file))
+
+    def test_exists__false(self):
+        fs = FileSystem()
+        self.assertFalse(fs.exists(self._missing_file))
+
+    def test_getcwd(self):
+        fs = FileSystem()
+        self.assertTrue(fs.exists(fs.getcwd()))
+
+    def test_isdir__true(self):
+        fs = FileSystem()
+        self.assertTrue(fs.isdir(self._this_dir))
+
+    def test_isdir__false(self):
+        fs = FileSystem()
+        self.assertFalse(fs.isdir(self._this_file))
+
+    def test_join(self):
+        fs = FileSystem()
+        self.assertEqual(fs.join('foo', 'bar'),
+                         os.path.join('foo', 'bar'))
+
+    def test_listdir(self):
+        fs = FileSystem()
+        with fs.mkdtemp(prefix='filesystem_unittest_') as d:
+            self.assertEqual(fs.listdir(d), [])
+            new_file = os.path.join(d, 'foo')
+            fs.write_text_file(new_file, u'foo')
+            self.assertEqual(fs.listdir(d), ['foo'])
+            os.remove(new_file)
+
+    def test_maybe_make_directory__success(self):
+        fs = FileSystem()
+
+        with fs.mkdtemp(prefix='filesystem_unittest_') as base_path:
+            sub_path = os.path.join(base_path, "newdir")
+            self.assertFalse(os.path.exists(sub_path))
+            self.assertFalse(fs.isdir(sub_path))
+
+            fs.maybe_make_directory(sub_path)
+            self.assertTrue(os.path.exists(sub_path))
+            self.assertTrue(fs.isdir(sub_path))
+
+            # Make sure we can re-create it.
+            fs.maybe_make_directory(sub_path)
+            self.assertTrue(os.path.exists(sub_path))
+            self.assertTrue(fs.isdir(sub_path))
+
+            # Clean up.
+            os.rmdir(sub_path)
+
+        self.assertFalse(os.path.exists(base_path))
+        self.assertFalse(fs.isdir(base_path))
+
+    def test_maybe_make_directory__failure(self):
+        # FIXME: os.chmod() doesn't work on Windows to set directories
+        # as readonly, so we skip this test for now.
+        if sys.platform in ('win32', 'cygwin'):
+            return
+
+        fs = FileSystem()
+        with fs.mkdtemp(prefix='filesystem_unittest_') as d:
+            # Remove write permissions on the parent directory.
+            os.chmod(d, stat.S_IRUSR)
+
+            # Now try to create a sub directory - should fail.
+            sub_dir = fs.join(d, 'subdir')
+            self.assertRaises(OSError, fs.maybe_make_directory, sub_dir)
+
+            # Clean up in case the test failed and we did create the
+            # directory.
+            if os.path.exists(sub_dir):
+                os.rmdir(sub_dir)
+
+    def test_read_and_write_text_file(self):
+        fs = FileSystem()
+        text_path = None
+
+        unicode_text_string = u'\u016An\u012Dc\u014Dde\u033D'
+        hex_equivalent = '\xC5\xAA\x6E\xC4\xAD\x63\xC5\x8D\x64\x65\xCC\xBD'
+        try:
+            text_path = tempfile.mktemp(prefix='tree_unittest_')
+            file = fs.open_text_file_for_writing(text_path)
+            file.write(unicode_text_string)
+            file.close()
+
+            file = fs.open_text_file_for_reading(text_path)
+            read_text = file.read()
+            file.close()
+
+            self.assertEqual(read_text, unicode_text_string)
+        finally:
+            if text_path and fs.isfile(text_path):
+                os.remove(text_path)
+
+    def test_read_and_write_file(self):
+        fs = FileSystem()
+        text_path = None
+        binary_path = None
+
+        unicode_text_string = u'\u016An\u012Dc\u014Dde\u033D'
+        hex_equivalent = '\xC5\xAA\x6E\xC4\xAD\x63\xC5\x8D\x64\x65\xCC\xBD'
+        try:
+            text_path = tempfile.mktemp(prefix='tree_unittest_')
+            binary_path = tempfile.mktemp(prefix='tree_unittest_')
+            fs.write_text_file(text_path, unicode_text_string)
+            contents = fs.read_binary_file(text_path)
+            self.assertEqual(contents, hex_equivalent)
+
+            fs.write_binary_file(binary_path, hex_equivalent)
+            text_contents = fs.read_text_file(binary_path)
+            self.assertEqual(text_contents, unicode_text_string)
+        finally:
+            if text_path and fs.isfile(text_path):
+                os.remove(text_path)
+            if binary_path and fs.isfile(binary_path):
+                os.remove(binary_path)
+
+    def test_read_binary_file__missing(self):
+        fs = FileSystem()
+        self.assertRaises(IOError, fs.read_binary_file, self._missing_file)
+
+    def test_read_text_file__missing(self):
+        fs = FileSystem()
+        self.assertRaises(IOError, fs.read_text_file, self._missing_file)
+
+    def test_remove_file_with_retry(self):
+        RealFileSystemTest._remove_failures = 2
+
+        def remove_with_exception(filename):
+            RealFileSystemTest._remove_failures -= 1
+            if RealFileSystemTest._remove_failures >= 0:
+                try:
+                    raise WindowsError
+                except NameError:
+                    raise FileSystem._WindowsError
+
+        fs = FileSystem()
+        self.assertTrue(fs.remove('filename', remove_with_exception))
+        self.assertEquals(-1, RealFileSystemTest._remove_failures)
+
+    def test_sep(self):
+        fs = FileSystem()
+
+        self.assertEquals(fs.sep, os.sep)
+        self.assertEquals(fs.join("foo", "bar"),
+                          os.path.join("foo", "bar"))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/common/system/logtesting.py b/Tools/Scripts/webkitpy/common/system/logtesting.py
new file mode 100644
index 0000000..e361cb5
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/logtesting.py
@@ -0,0 +1,258 @@
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+"""Supports the unit-testing of logging code.
+
+Provides support for unit-testing messages logged using the built-in
+logging module.
+
+Inherit from the LoggingTestCase class for basic testing needs.  For
+more advanced needs (e.g. unit-testing methods that configure logging),
+see the TestLogStream class, and perhaps also the LogTesting class.
+
+"""
+
+import logging
+import unittest
+
+
+class TestLogStream(object):
+
+    """Represents a file-like object for unit-testing logging.
+
+    This is meant for passing to the logging.StreamHandler constructor.
+    Log messages captured by instances of this object can be tested
+    using self.assertMessages() below.
+
+    """
+
+    def __init__(self, test_case):
+        """Create an instance.
+
+        Args:
+          test_case: A unittest.TestCase instance.
+
+        """
+        self._test_case = test_case
+        self.messages = []
+        """A list of log messages written to the stream."""
+
+    # Python documentation says that any object passed to the StreamHandler
+    # constructor should support write() and flush():
+    #
+    # http://docs.python.org/library/logging.html#module-logging.handlers
+    def write(self, message):
+        self.messages.append(message)
+
+    def flush(self):
+        pass
+
+    def assertMessages(self, messages):
+        """Assert that the given messages match the logged messages.
+
+        messages: A list of log message strings.
+
+        """
+        self._test_case.assertEquals(messages, self.messages)
+
+
+class LogTesting(object):
+
+    """Supports end-to-end unit-testing of log messages.
+
+        Sample usage:
+
+          class SampleTest(unittest.TestCase):
+
+              def setUp(self):
+                  self._log = LogTesting.setUp(self)  # Turn logging on.
+
+              def tearDown(self):
+                  self._log.tearDown()  # Turn off and reset logging.
+
+              def test_logging_in_some_method(self):
+                  call_some_method()  # Contains calls to _log.info(), etc.
+
+                  # Check the resulting log messages.
+                  self._log.assertMessages(["INFO: expected message #1",
+                                          "WARNING: expected message #2"])
+
+    """
+
+    def __init__(self, test_stream, handler):
+        """Create an instance.
+
+        This method should never be called directly.  Instances should
+        instead be created using the static setUp() method.
+
+        Args:
+          test_stream: A TestLogStream instance.
+          handler: The handler added to the logger.
+
+        """
+        self._test_stream = test_stream
+        self._handler = handler
+
+    @staticmethod
+    def _getLogger():
+        """Return the logger being tested."""
+        # It is possible we might want to return something other than
+        # the root logger in some special situation.  For now, the
+        # root logger seems to suffice.
+        return logging.getLogger()
+
+    @staticmethod
+    def setUp(test_case, logging_level=logging.INFO):
+        """Configure logging for unit testing.
+
+        Configures the root logger to log to a testing log stream.
+        Only messages logged at or above the given level are logged
+        to the stream.  Messages logged to the stream are formatted
+        in the following way, for example--
+
+        "INFO: This is a test log message."
+
+        This method should normally be called in the setUp() method
+        of a unittest.TestCase.  See the docstring of this class
+        for more details.
+
+        Returns:
+          A LogTesting instance.
+
+        Args:
+          test_case: A unittest.TestCase instance.
+          logging_level: An integer logging level that is the minimum level
+                         of log messages you would like to test.
+
+        """
+        stream = TestLogStream(test_case)
+        handler = logging.StreamHandler(stream)
+        handler.setLevel(logging_level)
+        formatter = logging.Formatter("%(levelname)s: %(message)s")
+        handler.setFormatter(formatter)
+
+        # Notice that we only change the root logger by adding a handler
+        # to it.  In particular, we do not reset its level using
+        # logger.setLevel().  This ensures that we have not interfered
+        # with how the code being tested may have configured the root
+        # logger.
+        logger = LogTesting._getLogger()
+        logger.addHandler(handler)
+
+        return LogTesting(stream, handler)
+
+    def tearDown(self):
+        """Assert there are no remaining log messages, and reset logging.
+
+        This method asserts that there are no more messages in the array of
+        log messages, and then restores logging to its original state.
+        This method should normally be called in the tearDown() method of a
+        unittest.TestCase.  See the docstring of this class for more details.
+
+        """
+        self.assertMessages([])
+        logger = LogTesting._getLogger()
+        logger.removeHandler(self._handler)
+
+    def messages(self):
+        """Return the current list of log messages."""
+        return self._test_stream.messages
+
+    # FIXME: Add a clearMessages() method for cases where the caller
+    #        deliberately doesn't want to assert every message.
+
+    # We clear the log messages after asserting since they are no longer
+    # needed after asserting.  This serves two purposes: (1) it simplifies
+    # the calling code when we want to check multiple logging calls in a
+    # single test method, and (2) it lets us check in the tearDown() method
+    # that there are no remaining log messages to be asserted.
+    #
+    # The latter ensures that no extra log messages are getting logged that
+    # the caller might not be aware of or may have forgotten to check for.
+    # This gets us a bit more mileage out of our tests without writing any
+    # additional code.
+    def assertMessages(self, messages):
+        """Assert the current array of log messages, and clear its contents.
+
+        Args:
+          messages: A list of log message strings.
+
+        """
+        try:
+            self._test_stream.assertMessages(messages)
+        finally:
+            # We want to clear the array of messages even in the case of
+            # an Exception (e.g. an AssertionError).  Otherwise, another
+            # AssertionError can occur in the tearDown() because the
+            # array might not have gotten emptied.
+            self._test_stream.messages = []
+
+
+# This class needs to inherit from unittest.TestCase.  Otherwise, the
+# setUp() and tearDown() methods will not get fired for test case classes
+# that inherit from this class -- even if the class inherits from *both*
+# unittest.TestCase and LoggingTestCase.
+#
+# FIXME: Rename this class to LoggingTestCaseBase to be sure that
+#        the unittest module does not interpret this class as a unittest
+#        test case itself.
+class LoggingTestCase(unittest.TestCase):
+
+    """Supports end-to-end unit-testing of log messages.
+
+        Sample usage:
+
+          class SampleTest(LoggingTestCase):
+
+              def test_logging_in_some_method(self):
+                  call_some_method()  # Contains calls to _log.info(), etc.
+
+                  # Check the resulting log messages.
+                  self.assertLog(["INFO: expected message #1",
+                                  "WARNING: expected message #2"])
+
+    """
+
+    def setUp(self):
+        self._log = LogTesting.setUp(self)
+
+    def tearDown(self):
+        self._log.tearDown()
+
+    def logMessages(self):
+        """Return the current list of log messages."""
+        return self._log.messages()
+
+    # FIXME: Add a clearMessages() method for cases where the caller
+    #        deliberately doesn't want to assert every message.
+
+    # See the code comments preceding LogTesting.assertMessages() for
+    # an explanation of why we clear the array of messages after
+    # asserting its contents.
+    def assertLog(self, messages):
+        """Assert the current array of log messages, and clear its contents.
+
+        Args:
+          messages: A list of log message strings.
+
+        """
+        self._log.assertMessages(messages)
diff --git a/Tools/Scripts/webkitpy/common/system/logutils.py b/Tools/Scripts/webkitpy/common/system/logutils.py
new file mode 100644
index 0000000..def3bec
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/logutils.py
@@ -0,0 +1,211 @@
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+"""Supports webkitpy logging."""
+
+# FIXME: Move this file to webkitpy/python24 since logging needs to
+#        be configured prior to running version-checking code.
+
+import logging
+import os
+import sys
+
+import webkitpy
+
+
+_log = logging.getLogger(__name__)
+
+# We set these directory paths lazily in get_logger() below.
+_scripts_dir = ""
+"""The normalized, absolute path to the ...Scripts directory."""
+
+_webkitpy_dir = ""
+"""The normalized, absolute path to the ...Scripts/webkitpy directory."""
+
+
+def _normalize_path(path):
+    """Return the given path normalized.
+
+    Converts a path to an absolute path, removes any trailing slashes,
+    removes any extension, and lower-cases it.
+
+    """
+    path = os.path.abspath(path)
+    path = os.path.normpath(path)
+    path = os.path.splitext(path)[0]  # Remove the extension, if any.
+    path = path.lower()
+
+    return path
+
+
+# Observe that the implementation of this function does not require
+# the use of any hard-coded strings like "webkitpy", etc.
+#
+# The main benefit this function has over using--
+#
+# _log = logging.getLogger(__name__)
+#
+# is that get_logger() returns the same value even if __name__ is
+# "__main__" -- i.e. even if the module is the script being executed
+# from the command-line.
+def get_logger(path):
+    """Return a logging.logger for the given path.
+
+    Returns:
+      A logger whose name is the name of the module corresponding to
+      the given path.  If the module is in webkitpy, the name is
+      the fully-qualified dotted module name beginning with webkitpy....
+      Otherwise, the name is the base name of the module (i.e. without
+      any dotted module name prefix).
+
+    Args:
+      path: The path of the module.  Normally, this parameter should be
+            the __file__ variable of the module.
+
+    Sample usage:
+
+      from webkitpy.common.system import logutils
+
+      _log = logutils.get_logger(__file__)
+
+    """
+    # Since we assign to _scripts_dir and _webkitpy_dir in this function,
+    # we need to declare them global.
+    global _scripts_dir
+    global _webkitpy_dir
+
+    path = _normalize_path(path)
+
+    # Lazily evaluate _webkitpy_dir and _scripts_dir.
+    if not _scripts_dir:
+        # The normalized, absolute path to ...Scripts/webkitpy/__init__.
+        webkitpy_path = _normalize_path(webkitpy.__file__)
+
+        _webkitpy_dir = os.path.split(webkitpy_path)[0]
+        _scripts_dir = os.path.split(_webkitpy_dir)[0]
+
+    if path.startswith(_webkitpy_dir):
+        # Remove the initial Scripts directory portion, so the path
+        # starts with /webkitpy, for example "/webkitpy/init/logutils".
+        path = path[len(_scripts_dir):]
+
+        parts = []
+        while True:
+            (path, tail) = os.path.split(path)
+            if not tail:
+                break
+            parts.insert(0, tail)
+
+        logger_name = ".".join(parts)  # For example, webkitpy.common.system.logutils.
+    else:
+        # The path is outside of webkitpy.  Default to the basename
+        # without the extension.
+        basename = os.path.basename(path)
+        logger_name = os.path.splitext(basename)[0]
+
+    return logging.getLogger(logger_name)
+
+
+def _default_handlers(stream, logging_level):
+    """Return a list of the default logging handlers to use.
+
+    Args:
+      stream: See the configure_logging() docstring.
+
+    """
+    # Create the filter.
+    def should_log(record):
+        """Return whether a logging.LogRecord should be logged."""
+        # FIXME: Enable the logging of autoinstall messages once
+        #        autoinstall is adjusted.  Currently, autoinstall logs
+        #        INFO messages when importing already-downloaded packages,
+        #        which is too verbose.
+        if record.name.startswith("webkitpy.thirdparty.autoinstall"):
+            return False
+        return True
+
+    logging_filter = logging.Filter()
+    logging_filter.filter = should_log
+
+    # Create the handler.
+    handler = logging.StreamHandler(stream)
+    if logging_level == logging.DEBUG:
+        formatter = logging.Formatter("%(name)s: [%(levelname)s] %(message)s")
+    else:
+        formatter = logging.Formatter("%(message)s")
+
+    handler.setFormatter(formatter)
+    handler.addFilter(logging_filter)
+
+    return [handler]
+
+
+def configure_logging(logging_level=None, logger=None, stream=None,
+                      handlers=None):
+    """Configure logging for standard purposes.
+
+    Returns:
+      A list of references to the logging handlers added to the root
+      logger.  This allows the caller to later remove the handlers
+      using logger.removeHandler.  This is useful primarily during unit
+      testing where the caller may want to configure logging temporarily
+      and then undo the configuring.
+
+    Args:
+      logging_level: The minimum logging level to log.  Defaults to
+                     logging.INFO.
+      logger: A logging.logger instance to configure.  This parameter
+              should be used only in unit tests.  Defaults to the
+              root logger.
+      stream: A file-like object to which to log used in creating the default
+              handlers.  The stream must define an "encoding" data attribute,
+              or else logging raises an error.  Defaults to sys.stderr.
+      handlers: A list of logging.Handler instances to add to the logger
+                being configured.  If this parameter is provided, then the
+                stream parameter is not used.
+
+    """
+    # If the stream does not define an "encoding" data attribute, the
+    # logging module can throw an error like the following:
+    #
+    # Traceback (most recent call last):
+    #   File "/System/Library/Frameworks/Python.framework/Versions/2.6/...
+    #         lib/python2.6/logging/__init__.py", line 761, in emit
+    #     self.stream.write(fs % msg.encode(self.stream.encoding))
+    # LookupError: unknown encoding: unknown
+    if logging_level is None:
+        logging_level = logging.INFO
+    if logger is None:
+        logger = logging.getLogger()
+    if stream is None:
+        stream = sys.stderr
+    if handlers is None:
+        handlers = _default_handlers(stream, logging_level)
+
+    logger.setLevel(logging_level)
+
+    for handler in handlers:
+        logger.addHandler(handler)
+
+    _log.debug("Debug logging enabled.")
+
+    return handlers
diff --git a/Tools/Scripts/webkitpy/common/system/logutils_unittest.py b/Tools/Scripts/webkitpy/common/system/logutils_unittest.py
new file mode 100644
index 0000000..72789eb
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/logutils_unittest.py
@@ -0,0 +1,158 @@
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+"""Unit tests for logutils.py."""
+
+import logging
+import os
+import unittest
+
+from webkitpy.common.system.logtesting import LogTesting
+from webkitpy.common.system.logtesting import TestLogStream
+from webkitpy.common.system import logutils
+
+
+class GetLoggerTest(unittest.TestCase):
+
+    """Tests get_logger()."""
+
+    def test_get_logger_in_webkitpy(self):
+        logger = logutils.get_logger(__file__)
+        self.assertEquals(logger.name, "webkitpy.common.system.logutils_unittest")
+
+    def test_get_logger_not_in_webkitpy(self):
+        # Temporarily change the working directory so that we
+        # can test get_logger() for a path outside of webkitpy.
+        working_directory = os.getcwd()
+        root_dir = "/"
+        os.chdir(root_dir)
+
+        logger = logutils.get_logger("/Tools/Scripts/test-webkitpy")
+        self.assertEquals(logger.name, "test-webkitpy")
+
+        logger = logutils.get_logger("/Tools/Scripts/test-webkitpy.py")
+        self.assertEquals(logger.name, "test-webkitpy")
+
+        os.chdir(working_directory)
+
+
+class ConfigureLoggingTestBase(unittest.TestCase):
+
+    """Base class for configure_logging() unit tests."""
+
+    def _logging_level(self):
+        raise Exception("Not implemented.")
+
+    def setUp(self):
+        log_stream = TestLogStream(self)
+
+        # Use a logger other than the root logger or one prefixed with
+        # "webkitpy." so as not to conflict with test-webkitpy logging.
+        logger = logging.getLogger("unittest")
+
+        # Configure the test logger not to pass messages along to the
+        # root logger.  This prevents test messages from being
+        # propagated to loggers used by test-webkitpy logging (e.g.
+        # the root logger).
+        logger.propagate = False
+
+        logging_level = self._logging_level()
+        self._handlers = logutils.configure_logging(logging_level=logging_level,
+                                                    logger=logger,
+                                                    stream=log_stream)
+        self._log = logger
+        self._log_stream = log_stream
+
+    def tearDown(self):
+        """Reset logging to its original state.
+
+        This method ensures that the logging configuration set up
+        for a unit test does not affect logging in other unit tests.
+
+        """
+        logger = self._log
+        for handler in self._handlers:
+            logger.removeHandler(handler)
+
+    def _assert_log_messages(self, messages):
+        """Assert that the logged messages equal the given messages."""
+        self._log_stream.assertMessages(messages)
+
+
+class ConfigureLoggingTest(ConfigureLoggingTestBase):
+
+    """Tests configure_logging() with the default logging level."""
+
+    def _logging_level(self):
+        return None
+
+    def test_info_message(self):
+        self._log.info("test message")
+        self._assert_log_messages(["test message\n"])
+
+    def test_debug_message(self):
+        self._log.debug("test message")
+        self._assert_log_messages([])
+
+    def test_below_threshold_message(self):
+        # We test the boundary case of a logging level equal to 19.
+        # In practice, we will probably only be calling log.debug(),
+        # which corresponds to a logging level of 10.
+        level = logging.INFO - 1  # Equals 19.
+        self._log.log(level, "test message")
+        self._assert_log_messages([])
+
+    def test_two_messages(self):
+        self._log.info("message1")
+        self._log.info("message2")
+        self._assert_log_messages(["message1\n",
+                                   "message2\n"])
+
+
+class ConfigureLoggingVerboseTest(ConfigureLoggingTestBase):
+    def _logging_level(self):
+        return logging.DEBUG
+
+    def test_info_message(self):
+        self._log.info("test message")
+        self._assert_log_messages(["unittest: [INFO] test message\n"])
+
+    def test_debug_message(self):
+        self._log.debug("test message")
+        self._assert_log_messages(["unittest: [DEBUG] test message\n"])
+
+class ConfigureLoggingCustomLevelTest(ConfigureLoggingTestBase):
+
+    """Tests configure_logging() with a custom logging level."""
+
+    _level = 36
+
+    def _logging_level(self):
+        return self._level
+
+    def test_logged_message(self):
+        self._log.log(self._level, "test message")
+        self._assert_log_messages(["test message\n"])
+
+    def test_below_threshold_message(self):
+        self._log.log(self._level - 1, "test message")
+        self._assert_log_messages([])
diff --git a/Tools/Scripts/webkitpy/common/system/outputcapture.py b/Tools/Scripts/webkitpy/common/system/outputcapture.py
new file mode 100644
index 0000000..78a12f0
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/outputcapture.py
@@ -0,0 +1,121 @@
+# Copyright (c) 2009, 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 for unittest support.  Used for capturing stderr/stdout.
+
+import logging
+import sys
+import unittest
+from StringIO import StringIO
+
+
+class OutputCapture(object):
+    # By default we capture the output to a stream. Other modules may override
+    # this function in order to do things like pass through the output. See
+    # webkitpy.test.main for an example.
+    @staticmethod
+    def stream_wrapper(stream):
+        return StringIO()
+
+    def __init__(self):
+        self.saved_outputs = dict()
+        self._log_level = logging.INFO
+
+    def set_log_level(self, log_level):
+        self._log_level = log_level
+        if hasattr(self, '_logs_handler'):
+            self._logs_handler.setLevel(self._log_level)
+
+    def _capture_output_with_name(self, output_name):
+        stream = getattr(sys, output_name)
+        captured_output = self.stream_wrapper(stream)
+        self.saved_outputs[output_name] = stream
+        setattr(sys, output_name, captured_output)
+        return captured_output
+
+    def _restore_output_with_name(self, output_name):
+        captured_output = getattr(sys, output_name).getvalue()
+        setattr(sys, output_name, self.saved_outputs[output_name])
+        del self.saved_outputs[output_name]
+        return captured_output
+
+    def capture_output(self):
+        self._logs = StringIO()
+        self._logs_handler = logging.StreamHandler(self._logs)
+        self._logs_handler.setLevel(self._log_level)
+        self._logger = logging.getLogger()
+        self._orig_log_level = self._logger.level
+        self._logger.addHandler(self._logs_handler)
+        self._logger.setLevel(min(self._log_level, self._orig_log_level))
+        return (self._capture_output_with_name("stdout"), self._capture_output_with_name("stderr"))
+
+    def restore_output(self):
+        self._logger.removeHandler(self._logs_handler)
+        self._logger.setLevel(self._orig_log_level)
+        self._logs_handler.flush()
+        self._logs.flush()
+        logs_string = self._logs.getvalue()
+        delattr(self, '_logs_handler')
+        delattr(self, '_logs')
+        return (self._restore_output_with_name("stdout"), self._restore_output_with_name("stderr"), logs_string)
+
+    def assert_outputs(self, testcase, function, args=[], kwargs={}, expected_stdout="", expected_stderr="", expected_exception=None, expected_logs=None):
+        self.capture_output()
+        try:
+            if expected_exception:
+                return_value = testcase.assertRaises(expected_exception, function, *args, **kwargs)
+            else:
+                return_value = function(*args, **kwargs)
+        finally:
+            (stdout_string, stderr_string, logs_string) = self.restore_output()
+
+        testcase.assertEqual(stdout_string, expected_stdout)
+        testcase.assertEqual(stderr_string, expected_stderr)
+        if expected_logs is not None:
+            testcase.assertEqual(logs_string, expected_logs)
+        # 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/Tools/Scripts/webkitpy/common/system/outputcapture_unittest.py b/Tools/Scripts/webkitpy/common/system/outputcapture_unittest.py
new file mode 100644
index 0000000..da4347c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/outputcapture_unittest.py
@@ -0,0 +1,59 @@
+# Copyright (C) 2011 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.
+
+import logging
+import unittest
+
+from webkitpy.common.system.outputcapture import OutputCapture
+
+
+_log = logging.getLogger(__name__)
+
+
+class OutputCaptureTest(unittest.TestCase):
+    def setUp(self):
+        self.output = OutputCapture()
+
+    def log_all_levels(self):
+        _log.info('INFO')
+        _log.warning('WARN')
+        _log.error('ERROR')
+        _log.critical('CRITICAL')
+
+    def assertLogged(self, expected_logs):
+        actual_stdout, actual_stderr, actual_logs = self.output.restore_output()
+        self.assertEqual('', actual_stdout)
+        self.assertEqual('', actual_stderr)
+        self.assertEqual(expected_logs, actual_logs)
+
+    def test_initial_log_level(self):
+        self.output.capture_output()
+        self.log_all_levels()
+        self.assertLogged('INFO\nWARN\nERROR\nCRITICAL\n')
+
+    def test_set_log_level(self):
+        self.output.set_log_level(logging.ERROR)
+        self.output.capture_output()
+        self.log_all_levels()
+        self.output.set_log_level(logging.WARN)
+        self.log_all_levels()
+        self.assertLogged('ERROR\nCRITICAL\nWARN\nERROR\nCRITICAL\n')
diff --git a/Tools/Scripts/webkitpy/common/system/path.py b/Tools/Scripts/webkitpy/common/system/path.py
new file mode 100644
index 0000000..e5a66bf
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/path.py
@@ -0,0 +1,135 @@
+# 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."""
+
+import atexit
+import subprocess
+import sys
+import threading
+import urllib
+
+
+def abspath_to_uri(platform, path):
+    """Converts a platform-specific absolute path to a file: URL."""
+    return "file:" + _escape(_convert_path(platform, path))
+
+
+def cygpath(path):
+    """Converts an absolute cygwin path to an absolute 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()
+        windows_path = self._child_process.stdout.readline().rstrip()
+        # Some versions of cygpath use lowercase drive letters while others
+        # use uppercase. We always convert to uppercase for consistency.
+        windows_path = '%s%s' % (windows_path[0].upper(), windows_path[1:])
+        return windows_path
+
+
+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(platform, path):
+    """Handles any os-specific path separators, mappings, etc."""
+    if platform.is_cygwin():
+        return _winpath_to_uri(cygpath(path))
+    if platform.is_win():
+        return _winpath_to_uri(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/Tools/Scripts/webkitpy/common/system/path_unittest.py b/Tools/Scripts/webkitpy/common/system/path_unittest.py
new file mode 100644
index 0000000..954d32d
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/path_unittest.py
@@ -0,0 +1,80 @@
+# 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
+
+from webkitpy.common.system.systemhost import SystemHost
+from webkitpy.common.system.platforminfo import PlatformInfo
+from webkitpy.common.system.platforminfo_mock import MockPlatformInfo
+from webkitpy.common.system import path
+
+class AbspathTest(unittest.TestCase):
+    def platforminfo(self):
+        return SystemHost().platform
+
+    def test_abspath_to_uri_cygwin(self):
+        if sys.platform != 'cygwin':
+            return
+        self.assertEquals(path.abspath_to_uri(self.platforminfo(), '/cygdrive/c/foo/bar.html'),
+                          'file:///C:/foo/bar.html')
+
+    def test_abspath_to_uri_unixy(self):
+        self.assertEquals(path.abspath_to_uri(MockPlatformInfo(), "/foo/bar.html"),
+                          'file:///foo/bar.html')
+
+    def test_abspath_to_uri_win(self):
+        if sys.platform != 'win32':
+            return
+        self.assertEquals(path.abspath_to_uri(self.platforminfo(), 'c:\\foo\\bar.html'),
+                         'file:///c:/foo/bar.html')
+
+    def test_abspath_to_uri_escaping_unixy(self):
+        self.assertEquals(path.abspath_to_uri(MockPlatformInfo(), '/foo/bar + baz%?.html'),
+                         'file:///foo/bar%20+%20baz%25%3F.html')
+
+        # Note that you can't have '?' in a filename on windows.
+    def test_abspath_to_uri_escaping_cygwin(self):
+        if sys.platform != 'cygwin':
+            return
+        self.assertEquals(path.abspath_to_uri(self.platforminfo(), '/cygdrive/c/foo/bar + baz%.html'),
+                          'file:///C:/foo/bar%20+%20baz%25.html')
+
+    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())
diff --git a/Tools/Scripts/webkitpy/common/system/platforminfo.py b/Tools/Scripts/webkitpy/common/system/platforminfo.py
new file mode 100644
index 0000000..b2451f5
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/platforminfo.py
@@ -0,0 +1,161 @@
+# Copyright (c) 2011 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 re
+import sys
+
+
+class PlatformInfo(object):
+    """This class provides a consistent (and mockable) interpretation of
+    system-specific values (like sys.platform and platform.mac_ver())
+    to be used by the rest of the webkitpy code base.
+
+    Public (static) properties:
+    -- os_name
+    -- os_version
+
+    Note that 'future' is returned for os_version if the operating system is
+    newer than one known to the code.
+    """
+
+    def __init__(self, sys_module, platform_module, executive):
+        self._executive = executive
+        self._platform_module = platform_module
+        self.os_name = self._determine_os_name(sys_module.platform)
+        if self.os_name == 'linux':
+            self.os_version = self._determine_linux_version()
+        if self.os_name == 'freebsd':
+            self.os_version = platform_module.release()
+        if self.os_name.startswith('mac'):
+            self.os_version = self._determine_mac_version(platform_module.mac_ver()[0])
+        if self.os_name.startswith('win'):
+            self.os_version = self._determine_win_version(self._win_version_tuple(sys_module))
+        self._is_cygwin = sys_module.platform == 'cygwin'
+
+    def is_mac(self):
+        return self.os_name == 'mac'
+
+    def is_win(self):
+        return self.os_name == 'win'
+
+    def is_cygwin(self):
+        return self._is_cygwin
+
+    def is_linux(self):
+        return self.os_name == 'linux'
+
+    def is_freebsd(self):
+        return self.os_name == 'freebsd'
+
+    def display_name(self):
+        # platform.platform() returns Darwin information for Mac, which is just confusing.
+        if self.is_mac():
+            return "Mac OS X %s" % self._platform_module.mac_ver()[0]
+
+        # Returns strings like:
+        # Linux-2.6.18-194.3.1.el5-i686-with-redhat-5.5-Final
+        # Windows-2008ServerR2-6.1.7600
+        return self._platform_module.platform()
+
+    def total_bytes_memory(self):
+        if self.is_mac():
+            return long(self._executive.run_command(["sysctl", "-n", "hw.memsize"]))
+        return None
+
+    def terminal_width(self):
+        """Returns sys.maxint if the width cannot be determined."""
+        try:
+            if self.is_win():
+                # From http://code.activestate.com/recipes/440694-determine-size-of-console-window-on-windows/
+                from ctypes import windll, create_string_buffer
+                handle = windll.kernel32.GetStdHandle(-12)  # -12 == stderr
+                console_screen_buffer_info = create_string_buffer(22)  # 22 == sizeof(console_screen_buffer_info)
+                if windll.kernel32.GetConsoleScreenBufferInfo(handle, console_screen_buffer_info):
+                    import struct
+                    _, _, _, _, _, left, _, right, _, _, _ = struct.unpack("hhhhHhhhhhh", console_screen_buffer_info.raw)
+                    # Note that we return 1 less than the width since writing into the rightmost column
+                    # automatically performs a line feed.
+                    return right - left
+                return sys.maxint
+            else:
+                import fcntl
+                import struct
+                import termios
+                packed = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, '\0' * 8)
+                _, columns, _, _ = struct.unpack('HHHH', packed)
+                return columns
+        except:
+            return sys.maxint
+
+    def _determine_os_name(self, sys_platform):
+        if sys_platform == 'darwin':
+            return 'mac'
+        if sys_platform.startswith('linux'):
+            return 'linux'
+        if sys_platform in ('win32', 'cygwin'):
+            return 'win'
+        if sys_platform.startswith('freebsd'):
+            return 'freebsd'
+        raise AssertionError('unrecognized platform string "%s"' % sys_platform)
+
+    def _determine_mac_version(self, mac_version_string):
+        release_version = mac_version_string.split('.')[1]
+        version_strings = {
+            '5': 'leopard',
+            '6': 'snowleopard',
+            '7': 'lion',
+            '8': 'mountainlion',
+        }
+        assert release_version >= min(version_strings.keys())
+        return version_strings.get(release_version, 'future')
+
+    def _determine_linux_version(self):
+        # FIXME: we ignore whatever the real version is and pretend it's lucid for now.
+        return 'lucid'
+
+    def _determine_win_version(self, win_version_tuple):
+        if win_version_tuple[:3] == (6, 1, 7600):
+            return '7sp0'
+        if win_version_tuple[:2] == (6, 0):
+            return 'vista'
+        if win_version_tuple[:2] == (5, 1):
+            return 'xp'
+        assert win_version_tuple[0] > 6 or win_version_tuple[1] >= 1, 'Unrecognized Windows version tuple: "%s"' % (win_version_tuple,)
+        return 'future'
+
+    def _win_version_tuple(self, sys_module):
+        if hasattr(sys_module, 'getwindowsversion'):
+            return sys_module.getwindowsversion()
+        return self._win_version_tuple_from_cmd()
+
+    def _win_version_tuple_from_cmd(self):
+        # Note that this should only ever be called on windows, so this should always work.
+        ver_output = self._executive.run_command(['cmd', '/c', 'ver'])
+        match_object = re.search(r'(?P<major>\d)\.(?P<minor>\d)\.(?P<build>\d+)', ver_output)
+        assert match_object, 'cmd returned an unexpected version string: ' + ver_output
+        return tuple(map(int, match_object.groups()))
diff --git a/Tools/Scripts/webkitpy/common/system/platforminfo_mock.py b/Tools/Scripts/webkitpy/common/system/platforminfo_mock.py
new file mode 100644
index 0000000..bc72810
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/platforminfo_mock.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2011 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 MockPlatformInfo(object):
+    def __init__(self, os_name='mac', os_version='snowleopard'):
+        self.os_name = os_name
+        self.os_version = os_version
+
+    def is_mac(self):
+        return self.os_name == 'mac'
+
+    def is_linux(self):
+        return self.os_name == 'linux'
+
+    def is_win(self):
+        return self.os_name == 'win'
+
+    def is_cygwin(self):
+        return self.os_name == 'cygwin'
+
+    def is_freebsd(self):
+        return self.os_name == 'freebsd'
+
+    def display_name(self):
+        return "MockPlatform 1.0"
+
+    def total_bytes_memory(self):
+        return 3 * 1024 * 1024 * 1024  # 3GB is a reasonable amount of ram to mock.
+
+    def terminal_width(self):
+        return 80
diff --git a/Tools/Scripts/webkitpy/common/system/platforminfo_unittest.py b/Tools/Scripts/webkitpy/common/system/platforminfo_unittest.py
new file mode 100644
index 0000000..a2b4255
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/platforminfo_unittest.py
@@ -0,0 +1,185 @@
+# Copyright (C) 2011 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 platform
+import sys
+import unittest
+
+from webkitpy.common.system.executive import Executive
+from webkitpy.common.system.executive_mock import MockExecutive, MockExecutive2
+from webkitpy.common.system.platforminfo import PlatformInfo
+
+
+def fake_sys(platform_str='darwin', windows_version_tuple=None):
+
+    class FakeSysModule(object):
+        platform = platform_str
+        if windows_version_tuple:
+            getwindowsversion = lambda x: windows_version_tuple
+
+    return FakeSysModule()
+
+
+def fake_platform(mac_version_string='10.6.3', release_string='bar'):
+
+    class FakePlatformModule(object):
+        def mac_ver(self):
+            return tuple([mac_version_string, tuple(['', '', '']), 'i386'])
+
+        def platform(self):
+            return 'foo'
+
+        def release(self):
+            return release_string
+
+    return FakePlatformModule()
+
+
+def fake_executive(output=None):
+    if output:
+        return MockExecutive2(output=output)
+    return MockExecutive2(exception=SystemError)
+
+
+class TestPlatformInfo(unittest.TestCase):
+    def make_info(self, sys_module=None, platform_module=None, executive=None):
+        return PlatformInfo(sys_module or fake_sys(), platform_module or fake_platform(), executive or fake_executive())
+
+    # FIXME: This should be called integration_test_real_code(), but integration tests aren't
+    # yet run by default and there's no reason not to run this everywhere by default.
+    def test_real_code(self):
+        # This test makes sure the real (unmocked) code actually works.
+        info = PlatformInfo(sys, platform, Executive())
+        self.assertNotEquals(info.os_name, '')
+        self.assertNotEquals(info.os_version, '')
+        self.assertNotEquals(info.display_name(), '')
+        self.assertTrue(info.is_mac() or info.is_win() or info.is_linux() or info.is_freebsd())
+        self.assertNotEquals(info.terminal_width(), None)
+
+        if info.is_mac():
+            self.assertTrue(info.total_bytes_memory() > 0)
+        else:
+            self.assertEquals(info.total_bytes_memory(), None)
+
+    def test_os_name_and_wrappers(self):
+        info = self.make_info(fake_sys('linux2'))
+        self.assertTrue(info.is_linux())
+        self.assertFalse(info.is_mac())
+        self.assertFalse(info.is_win())
+        self.assertFalse(info.is_freebsd())
+
+        info = self.make_info(fake_sys('linux3'))
+        self.assertTrue(info.is_linux())
+        self.assertFalse(info.is_mac())
+        self.assertFalse(info.is_win())
+        self.assertFalse(info.is_freebsd())
+
+        info = self.make_info(fake_sys('darwin'), fake_platform('10.6.3'))
+        self.assertEquals(info.os_name, 'mac')
+        self.assertFalse(info.is_linux())
+        self.assertTrue(info.is_mac())
+        self.assertFalse(info.is_win())
+        self.assertFalse(info.is_freebsd())
+
+        info = self.make_info(fake_sys('win32', tuple([6, 1, 7600])))
+        self.assertEquals(info.os_name, 'win')
+        self.assertFalse(info.is_linux())
+        self.assertFalse(info.is_mac())
+        self.assertTrue(info.is_win())
+        self.assertFalse(info.is_freebsd())
+
+        info = self.make_info(fake_sys('cygwin'), executive=fake_executive('6.1.7600'))
+        self.assertEquals(info.os_name, 'win')
+        self.assertFalse(info.is_linux())
+        self.assertFalse(info.is_mac())
+        self.assertTrue(info.is_win())
+        self.assertFalse(info.is_freebsd())
+
+        info = self.make_info(fake_sys('freebsd8'))
+        self.assertEquals(info.os_name, 'freebsd')
+        self.assertFalse(info.is_linux())
+        self.assertFalse(info.is_mac())
+        self.assertFalse(info.is_win())
+        self.assertTrue(info.is_freebsd())
+
+        self.assertRaises(AssertionError, self.make_info, fake_sys('vms'))
+
+    def test_os_version(self):
+        self.assertRaises(AssertionError, self.make_info, fake_sys('darwin'), fake_platform('10.4.3'))
+        self.assertEquals(self.make_info(fake_sys('darwin'), fake_platform('10.5.1')).os_version, 'leopard')
+        self.assertEquals(self.make_info(fake_sys('darwin'), fake_platform('10.6.1')).os_version, 'snowleopard')
+        self.assertEquals(self.make_info(fake_sys('darwin'), fake_platform('10.7.1')).os_version, 'lion')
+        self.assertEquals(self.make_info(fake_sys('darwin'), fake_platform('10.8.1')).os_version, 'mountainlion')
+        self.assertEquals(self.make_info(fake_sys('darwin'), fake_platform('10.9.0')).os_version, 'future')
+
+        self.assertEquals(self.make_info(fake_sys('linux2')).os_version, 'lucid')
+
+        self.assertEquals(self.make_info(fake_sys('freebsd8'), fake_platform('', '8.3-PRERELEASE')).os_version, '8.3-PRERELEASE')
+        self.assertEquals(self.make_info(fake_sys('freebsd9'), fake_platform('', '9.0-RELEASE')).os_version, '9.0-RELEASE')
+
+        self.assertRaises(AssertionError, self.make_info, fake_sys('win32', tuple([5, 0, 1234])))
+        self.assertEquals(self.make_info(fake_sys('win32', tuple([6, 2, 1234]))).os_version, 'future')
+        self.assertEquals(self.make_info(fake_sys('win32', tuple([6, 1, 7600]))).os_version, '7sp0')
+        self.assertEquals(self.make_info(fake_sys('win32', tuple([6, 0, 1234]))).os_version, 'vista')
+        self.assertEquals(self.make_info(fake_sys('win32', tuple([5, 1, 1234]))).os_version, 'xp')
+
+        self.assertRaises(AssertionError, self.make_info, fake_sys('win32'), executive=fake_executive('5.0.1234'))
+        self.assertEquals(self.make_info(fake_sys('cygwin'), executive=fake_executive('6.2.1234')).os_version, 'future')
+        self.assertEquals(self.make_info(fake_sys('cygwin'), executive=fake_executive('6.1.7600')).os_version, '7sp0')
+        self.assertEquals(self.make_info(fake_sys('cygwin'), executive=fake_executive('6.0.1234')).os_version, 'vista')
+        self.assertEquals(self.make_info(fake_sys('cygwin'), executive=fake_executive('5.1.1234')).os_version, 'xp')
+
+    def test_display_name(self):
+        info = self.make_info(fake_sys('darwin'))
+        self.assertNotEquals(info.display_name(), '')
+
+        info = self.make_info(fake_sys('win32', tuple([6, 1, 7600])))
+        self.assertNotEquals(info.display_name(), '')
+
+        info = self.make_info(fake_sys('linux2'))
+        self.assertNotEquals(info.display_name(), '')
+
+        info = self.make_info(fake_sys('freebsd9'))
+        self.assertNotEquals(info.display_name(), '')
+
+    def test_total_bytes_memory(self):
+        info = self.make_info(fake_sys('darwin'), fake_platform('10.6.3'), fake_executive('1234'))
+        self.assertEquals(info.total_bytes_memory(), 1234)
+
+        info = self.make_info(fake_sys('win32', tuple([6, 1, 7600])))
+        self.assertEquals(info.total_bytes_memory(), None)
+
+        info = self.make_info(fake_sys('linux2'))
+        self.assertEquals(info.total_bytes_memory(), None)
+
+        info = self.make_info(fake_sys('freebsd9'))
+        self.assertEquals(info.total_bytes_memory(), None)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/common/system/stack_utils.py b/Tools/Scripts/webkitpy/common/system/stack_utils.py
new file mode 100644
index 0000000..a343807
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/stack_utils.py
@@ -0,0 +1,67 @@
+# Copyright (C) 2011 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.
+
+"""Simple routines for logging, obtaining thread stack information."""
+
+import sys
+import traceback
+
+
+def log_thread_state(logger, name, thread_id, msg=''):
+    """Log information about the given thread state."""
+    stack = _find_thread_stack(thread_id)
+    assert(stack is not None)
+    logger("")
+    logger("%s (tid %d) %s" % (name, thread_id, msg))
+    _log_stack(logger, stack)
+    logger("")
+
+
+def _find_thread_stack(thread_id):
+    """Returns a stack object that can be used to dump a stack trace for
+    the given thread id (or None if the id is not found)."""
+    for tid, stack in sys._current_frames().items():
+        if tid == thread_id:
+            return stack
+    return None
+
+
+def _log_stack(logger, stack):
+    """Log a stack trace to the logger callback."""
+    for filename, lineno, name, line in traceback.extract_stack(stack):
+        logger('File: "%s", line %d, in %s' % (filename, lineno, name))
+        if line:
+            logger('  %s' % line.strip())
+
+
+def log_traceback(logger, tb):
+    stack = traceback.extract_tb(tb)
+    for frame_str in traceback.format_list(stack):
+        for line in frame_str.split('\n'):
+            if line:
+                logger("  %s" % line)
diff --git a/Tools/Scripts/webkitpy/common/system/stack_utils_unittest.py b/Tools/Scripts/webkitpy/common/system/stack_utils_unittest.py
new file mode 100644
index 0000000..625acf2
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/stack_utils_unittest.py
@@ -0,0 +1,72 @@
+# Copyright (C) 2011 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 sys
+import unittest
+
+from webkitpy.common.system import outputcapture
+from webkitpy.common.system import stack_utils
+
+
+def current_thread_id():
+    thread_id, _ = sys._current_frames().items()[0]
+    return thread_id
+
+
+class StackUtilsTest(unittest.TestCase):
+    def test_find_thread_stack_found(self):
+        thread_id = current_thread_id()
+        found_stack = stack_utils._find_thread_stack(thread_id)
+        self.assertNotEqual(found_stack, None)
+
+    def test_find_thread_stack_not_found(self):
+        found_stack = stack_utils._find_thread_stack(0)
+        self.assertEqual(found_stack, None)
+
+    def test_log_thread_state(self):
+        msgs = []
+
+        def logger(msg):
+            msgs.append(msg)
+
+        thread_id = current_thread_id()
+        stack_utils.log_thread_state(logger, "test-thread", thread_id,
+                                     "is tested")
+        self.assertTrue(msgs)
+
+    def test_log_traceback(self):
+        msgs = []
+
+        def logger(msg):
+            msgs.append(msg)
+
+        try:
+            raise ValueError
+        except:
+            stack_utils.log_traceback(logger, sys.exc_info()[2])
+        self.assertTrue(msgs)
diff --git a/Tools/Scripts/webkitpy/common/system/systemhost.py b/Tools/Scripts/webkitpy/common/system/systemhost.py
new file mode 100644
index 0000000..dfec68b
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/systemhost.py
@@ -0,0 +1,48 @@
+# Copyright (c) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import platform
+import sys
+
+from webkitpy.common.system import environment, executive, file_lock, filesystem, platforminfo, user, workspace
+
+
+class SystemHost(object):
+    def __init__(self):
+        self.executive = executive.Executive()
+        self.filesystem = filesystem.FileSystem()
+        self.user = user.User()
+        self.platform = platforminfo.PlatformInfo(sys, platform, self.executive)
+        self.workspace = workspace.Workspace(self.filesystem, self.executive)
+
+    def copy_current_environment(self):
+        return environment.Environment(os.environ.copy())
+
+    def make_file_lock(self, path):
+        return file_lock.FileLock(path)
diff --git a/Tools/Scripts/webkitpy/common/system/systemhost_mock.py b/Tools/Scripts/webkitpy/common/system/systemhost_mock.py
new file mode 100644
index 0000000..a529f34
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/systemhost_mock.py
@@ -0,0 +1,56 @@
+    # Copyright (c) 2011 Google Inc. All rights reserved.
+    #
+    # Redistribution and use in source and binary forms, with or without
+    # modification, are permitted provided that the following conditions are
+    # met:
+    #
+    #     * Redistributions of source code must retain the above copyright
+    # notice, this list of conditions and the following disclaimer.
+    #     * Redistributions in binary form must reproduce the above
+    # copyright notice, this list of conditions and the following disclaimer
+    # in the documentation and/or other materials provided with the
+    # distribution.
+    #     * Neither the name of Google Inc. nor the names of its
+    # contributors may be used to endorse or promote products derived from
+    # this software without specific prior written permission.
+    #
+    # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+    # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+    # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+    # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+    # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+    # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+    # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+    # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+    # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+    # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+    # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.system.environment import Environment
+from webkitpy.common.system.executive_mock import MockExecutive
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.file_lock_mock import MockFileLock
+from webkitpy.common.system.platforminfo_mock import MockPlatformInfo
+from webkitpy.common.system.user_mock import MockUser
+from webkitpy.common.system.workspace_mock import MockWorkspace
+
+
+class MockSystemHost(object):
+    def __init__(self, log_executive=False, executive_throws_when_run=None, os_name=None, os_version=None, executive=None, filesystem=None):
+        self.executive = executive or MockExecutive(should_log=log_executive, should_throw_when_run=executive_throws_when_run)
+        self.filesystem = filesystem or MockFileSystem()
+        self.user = MockUser()
+        self.platform = MockPlatformInfo()
+        if os_name:
+            self.platform.os_name = os_name
+        if os_version:
+            self.platform.os_version = os_version
+
+        # FIXME: Should this take pointers to the filesystem and the executive?
+        self.workspace = MockWorkspace()
+
+    def copy_current_environment(self):
+        return Environment({"MOCK_ENVIRON_COPY": '1'})
+
+    def make_file_lock(self, path):
+        return MockFileLock(path)
diff --git a/Tools/Scripts/webkitpy/common/system/urlfetcher.py b/Tools/Scripts/webkitpy/common/system/urlfetcher.py
new file mode 100644
index 0000000..2d9e5ec
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/urlfetcher.py
@@ -0,0 +1,55 @@
+# Copyright (C) 2011 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.
+
+"""Wrapper module for fetching URLs."""
+
+import urllib
+
+
+class UrlFetcher(object):
+    """Class with restricted interface to fetch URLs (makes testing easier)"""
+    def __init__(self, filesystem):
+        self._filesystem = filesystem
+
+    def fetch(self, url):
+        """Fetches the contents of the URL as a string."""
+        file_object = urllib.urlopen(url)
+        content = file_object.read()
+        file_object.close()
+        return content
+
+    def fetch_into_file(self, url):
+        """Fetches the contents of the URL into a temporary file and return the filename.
+
+        This is the equivalent of urllib.retrieve() except that we don't return any headers.
+        """
+        file_object, filename = self._filesystem.open_binary_tempfile('-fetched')
+        contents = self.fetch(url)
+        file_object.write(contents)
+        file_object.close()
+        return filename
diff --git a/Tools/Scripts/webkitpy/common/system/urlfetcher_mock.py b/Tools/Scripts/webkitpy/common/system/urlfetcher_mock.py
new file mode 100644
index 0000000..e8a7532
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/urlfetcher_mock.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2011 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.
+
+
+def make_fetcher_cls(urls):
+    """UrlFetcher factory routine that simulates network access
+    using a dict of URLs -> contents."""
+    class MockFetcher(object):
+        def __init__(self, filesystem):
+            self._filesystem = filesystem
+
+        def fetch(self, url):
+            return urls[url]
+
+        def fetch_into_file(self, url):
+            f, fn = self._filesystem.open_binary_tempfile('mockfetcher')
+            f.write(self.fetch(url))
+            f.close()
+            return fn
+
+    return MockFetcher
diff --git a/Tools/Scripts/webkitpy/common/system/user.py b/Tools/Scripts/webkitpy/common/system/user.py
new file mode 100644
index 0000000..c49429c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/user.py
@@ -0,0 +1,175 @@
+# Copyright (c) 2009, 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 getpass
+import logging
+import os
+import platform
+import re
+import shlex
+import subprocess
+import sys
+import webbrowser
+
+from webkitpy.common.system.executive import Executive
+from webkitpy.common.system.platforminfo import PlatformInfo
+
+
+_log = logging.getLogger(__name__)
+
+
+try:
+    import readline
+except ImportError:
+    if sys.platform != "win32":
+        # There is no readline module for win32, not much to do except cry.
+        _log.warn("Unable to import readline.")
+
+
+class User(object):
+    DEFAULT_NO = 'n'
+    DEFAULT_YES = 'y'
+
+    def __init__(self, platforminfo=None):
+        # We cannot get the PlatformInfo object from a SystemHost because
+        # User is part of SystemHost itself.
+        self._platforminfo = platforminfo or PlatformInfo(sys, platform, Executive())
+
+    # FIXME: These are @classmethods because bugzilla.py doesn't have a Tool object (thus no User instance).
+    @classmethod
+    def prompt(cls, message, repeat=1, raw_input=raw_input):
+        response = None
+        while (repeat and not response):
+            repeat -= 1
+            response = raw_input(message)
+        return response
+
+    @classmethod
+    def prompt_password(cls, message, repeat=1):
+        return cls.prompt(message, repeat=repeat, raw_input=getpass.getpass)
+
+    @classmethod
+    def prompt_with_multiple_lists(cls, list_title, subtitles, lists, can_choose_multiple=False, raw_input=raw_input):
+        item_index = 0
+        cumulated_list = []
+        print list_title
+        for i in range(len(subtitles)):
+            print "\n" + subtitles[i]
+            for item in lists[i]:
+                item_index += 1
+                print "%2d. %s" % (item_index, item)
+            cumulated_list += lists[i]
+        return cls._wait_on_list_response(cumulated_list, can_choose_multiple, raw_input)
+
+    @classmethod
+    def _wait_on_list_response(cls, list_items, can_choose_multiple, raw_input):
+        while True:
+            if can_choose_multiple:
+                response = cls.prompt("Enter one or more numbers (comma-separated) or ranges (e.g. 3-7), or \"all\": ", raw_input=raw_input)
+                if not response.strip() or response == "all":
+                    return list_items
+
+                try:
+                    indices = []
+                    for value in re.split("\s*,\s*", response):
+                        parts = value.split('-')
+                        if len(parts) == 2:
+                            indices += range(int(parts[0]) - 1, int(parts[1]))
+                        else:
+                            indices.append(int(value) - 1)
+                except ValueError, err:
+                    continue
+
+                return [list_items[i] for i in indices]
+            else:
+                try:
+                    result = int(cls.prompt("Enter a number: ", raw_input=raw_input)) - 1
+                except ValueError, err:
+                    continue
+                return list_items[result]
+
+    @classmethod
+    def prompt_with_list(cls, list_title, list_items, can_choose_multiple=False, raw_input=raw_input):
+        print list_title
+        i = 0
+        for item in list_items:
+            i += 1
+            print "%2d. %s" % (i, item)
+        return cls._wait_on_list_response(list_items, can_choose_multiple, raw_input)
+
+    def edit(self, files):
+        editor = os.environ.get("EDITOR") or "vi"
+        args = shlex.split(editor)
+        # 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 self._platforminfo.is_mac():
+            # On Mac we support editing ChangeLogs using an application.
+            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."
+            self._warn_if_application_is_xcode(edit_application)
+            subprocess.call(["open", "-W", "-n", "-a"] + args + files)
+            return
+        self.edit(files)
+
+    def page(self, message):
+        pager = os.environ.get("PAGER") or "less"
+        try:
+            # Note: Not thread safe: http://bugs.python.org/issue2320
+            child_process = subprocess.Popen([pager], stdin=subprocess.PIPE)
+            child_process.communicate(input=message)
+        except IOError, e:
+            pass
+
+    def confirm(self, message=None, default=DEFAULT_YES, raw_input=raw_input):
+        if not message:
+            message = "Continue?"
+        choice = {'y': 'Y/n', 'n': 'y/N'}[default]
+        response = raw_input("%s [%s]: " % (message, choice))
+        if not response:
+            response = default
+        return response.lower() == 'y'
+
+    def can_open_url(self):
+        try:
+            webbrowser.get()
+            return True
+        except webbrowser.Error, e:
+            return False
+
+    def open_url(self, url):
+        if not self.can_open_url():
+            _log.warn("Failed to open %s" % url)
+        webbrowser.open(url)
diff --git a/Tools/Scripts/webkitpy/common/system/user_mock.py b/Tools/Scripts/webkitpy/common/system/user_mock.py
new file mode 100644
index 0000000..16f79a0
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/user_mock.py
@@ -0,0 +1,66 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.system.deprecated_logging import log
+
+
+class MockUser(object):
+
+    @classmethod
+    def prompt(cls, message, repeat=1, raw_input=raw_input):
+        return "Mock user response"
+
+    @classmethod
+    def prompt_with_list(cls, list_title, list_items, can_choose_multiple=False, raw_input=raw_input):
+        pass
+
+    def __init__(self):
+        self.opened_urls = []
+
+    def edit(self, files):
+        pass
+
+    def edit_changelog(self, files):
+        pass
+
+    def page(self, message):
+        pass
+
+    def confirm(self, message=None, default='y'):
+        log(message)
+        return default == 'y'
+
+    def can_open_url(self):
+        return True
+
+    def open_url(self, url):
+        self.opened_urls.append(url)
+        if url.startswith("file://"):
+            log("MOCK: user.open_url: file://...")
+            return
+        log("MOCK: user.open_url: %s" % url)
diff --git a/Tools/Scripts/webkitpy/common/system/user_unittest.py b/Tools/Scripts/webkitpy/common/system/user_unittest.py
new file mode 100644
index 0000000..86b9db7
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/user_unittest.py
@@ -0,0 +1,139 @@
+# Copyright (C) 2010 Research in Motion Ltd. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Research in Motion Ltd. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.system.user import User
+
+class UserTest(unittest.TestCase):
+
+    example_user_response = "example user response"
+
+    def test_prompt_repeat(self):
+        self.repeatsRemaining = 2
+        def mock_raw_input(message):
+            self.repeatsRemaining -= 1
+            if not self.repeatsRemaining:
+                return UserTest.example_user_response
+            return None
+        self.assertEqual(User.prompt("input", repeat=self.repeatsRemaining, raw_input=mock_raw_input), UserTest.example_user_response)
+
+    def test_prompt_when_exceeded_repeats(self):
+        self.repeatsRemaining = 2
+        def mock_raw_input(message):
+            self.repeatsRemaining -= 1
+            return None
+        self.assertEqual(User.prompt("input", repeat=self.repeatsRemaining, raw_input=mock_raw_input), None)
+
+    def test_prompt_with_multiple_lists(self):
+        def run_prompt_test(inputs, expected_result, can_choose_multiple=False):
+            def mock_raw_input(message):
+                return inputs.pop(0)
+            output_capture = OutputCapture()
+            actual_result = output_capture.assert_outputs(
+                self,
+                User.prompt_with_multiple_lists,
+                args=["title", ["subtitle1", "subtitle2"], [["foo", "bar"], ["foobar", "barbaz", "foobaz"]]],
+                kwargs={"can_choose_multiple": can_choose_multiple, "raw_input": mock_raw_input},
+                expected_stdout="title\n\nsubtitle1\n 1. foo\n 2. bar\n\nsubtitle2\n 3. foobar\n 4. barbaz\n 5. foobaz\n")
+            self.assertEqual(actual_result, expected_result)
+            self.assertEqual(len(inputs), 0)
+
+        run_prompt_test(["1"], "foo")
+        run_prompt_test(["badinput", "2"], "bar")
+        run_prompt_test(["3"], "foobar")
+        run_prompt_test(["4"], "barbaz")
+        run_prompt_test(["5"], "foobaz")
+
+        run_prompt_test(["1,2"], ["foo", "bar"], can_choose_multiple=True)
+        run_prompt_test(["1-3"], ["foo", "bar", "foobar"], can_choose_multiple=True)
+        run_prompt_test(["1-2,3"], ["foo", "bar", "foobar"], can_choose_multiple=True)
+        run_prompt_test(["2-1,3"], ["foobar"], can_choose_multiple=True)
+        run_prompt_test(["  1,  2   "], ["foo", "bar"], can_choose_multiple=True)
+        run_prompt_test(["all"], ["foo", "bar", 'foobar', 'barbaz', 'foobaz'], can_choose_multiple=True)
+        run_prompt_test([""], ["foo", "bar", 'foobar', 'barbaz', 'foobaz'], can_choose_multiple=True)
+        run_prompt_test(["  "], ["foo", "bar", 'foobar', 'barbaz', 'foobaz'], can_choose_multiple=True)
+        run_prompt_test(["badinput", "all"], ["foo", "bar", 'foobar', 'barbaz', 'foobaz'], can_choose_multiple=True)
+
+    def test_prompt_with_list(self):
+        def run_prompt_test(inputs, expected_result, can_choose_multiple=False):
+            def mock_raw_input(message):
+                return inputs.pop(0)
+            output_capture = OutputCapture()
+            actual_result = output_capture.assert_outputs(
+                self,
+                User.prompt_with_list,
+                args=["title", ["foo", "bar"]],
+                kwargs={"can_choose_multiple": can_choose_multiple, "raw_input": mock_raw_input},
+                expected_stdout="title\n 1. foo\n 2. bar\n")
+            self.assertEqual(actual_result, expected_result)
+            self.assertEqual(len(inputs), 0)
+
+        run_prompt_test(["1"], "foo")
+        run_prompt_test(["badinput", "2"], "bar")
+
+        run_prompt_test(["1,2"], ["foo", "bar"], can_choose_multiple=True)
+        run_prompt_test(["  1,  2   "], ["foo", "bar"], can_choose_multiple=True)
+        run_prompt_test(["all"], ["foo", "bar"], can_choose_multiple=True)
+        run_prompt_test([""], ["foo", "bar"], can_choose_multiple=True)
+        run_prompt_test(["  "], ["foo", "bar"], can_choose_multiple=True)
+        run_prompt_test(["badinput", "all"], ["foo", "bar"], can_choose_multiple=True)
+
+    def test_confirm(self):
+        test_cases = (
+            (("Continue? [Y/n]: ", True), (User.DEFAULT_YES, 'y')),
+            (("Continue? [Y/n]: ", False), (User.DEFAULT_YES, 'n')),
+            (("Continue? [Y/n]: ", True), (User.DEFAULT_YES, '')),
+            (("Continue? [Y/n]: ", False), (User.DEFAULT_YES, 'q')),
+            (("Continue? [y/N]: ", True), (User.DEFAULT_NO, 'y')),
+            (("Continue? [y/N]: ", False), (User.DEFAULT_NO, 'n')),
+            (("Continue? [y/N]: ", False), (User.DEFAULT_NO, '')),
+            (("Continue? [y/N]: ", False), (User.DEFAULT_NO, 'q')),
+        )
+        for test_case in test_cases:
+            expected, inputs = test_case
+
+            def mock_raw_input(message):
+                self.assertEquals(expected[0], message)
+                return inputs[1]
+
+            result = User().confirm(default=inputs[0],
+                                    raw_input=mock_raw_input)
+            self.assertEquals(expected[1], result)
+
+    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/Tools/Scripts/webkitpy/common/system/workspace.py b/Tools/Scripts/webkitpy/common/system/workspace.py
new file mode 100644
index 0000000..6868376
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/workspace.py
@@ -0,0 +1,73 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# A home for file logic which should sit above FileSystem, but
+# below more complicated objects.
+
+import logging
+import zipfile
+
+from webkitpy.common.system.executive import ScriptError
+
+
+_log = logging.getLogger(__name__)
+
+
+class Workspace(object):
+    def __init__(self, filesystem, executive):
+        self._filesystem = filesystem
+        self._executive = executive  # FIXME: Remove if create_zip is moved to python.
+
+    def find_unused_filename(self, directory, name, extension, search_limit=100):
+        for count in range(search_limit):
+            if count:
+                target_name = "%s-%s.%s" % (name, count, extension)
+            else:
+                target_name = "%s.%s" % (name, extension)
+            target_path = self._filesystem.join(directory, target_name)
+            if not self._filesystem.exists(target_path):
+                return target_path
+        # If we can't find an unused name in search_limit tries, just give up.
+        return None
+
+    def create_zip(self, zip_path, source_path, zip_class=zipfile.ZipFile):
+        # It's possible to create zips with Python:
+        # zip_file = ZipFile(zip_path, 'w')
+        # for root, dirs, files in os.walk(source_path):
+        #     for path in files:
+        #         absolute_path = os.path.join(root, path)
+        #         zip_file.write(os.path.relpath(path, source_path))
+        # However, getting the paths, encoding and compression correct could be non-trivial.
+        # So, for now we depend on the environment having "zip" installed (likely fails on Win32)
+        try:
+            self._executive.run_command(['zip', '-9', '-r', zip_path, '.'], cwd=source_path)
+        except ScriptError, e:
+            _log.error("Workspace.create_zip failed:\n%s" % e.message_with_output())
+            return None
+
+        return zip_class(zip_path)
diff --git a/Tools/Scripts/webkitpy/common/system/workspace_mock.py b/Tools/Scripts/webkitpy/common/system/workspace_mock.py
new file mode 100644
index 0000000..005f86c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/workspace_mock.py
@@ -0,0 +1,35 @@
+# Copyright (C) 2011 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 MockWorkspace(object):
+    def find_unused_filename(self, directory, name, extension, search_limit=10):
+        return "%s/%s.%s" % (directory, name, extension)
+
+    def create_zip(self, zip_path, source_path):
+        return object()  # Something that is not None
diff --git a/Tools/Scripts/webkitpy/common/system/workspace_unittest.py b/Tools/Scripts/webkitpy/common/system/workspace_unittest.py
new file mode 100644
index 0000000..49094ac
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/workspace_unittest.py
@@ -0,0 +1,67 @@
+# 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.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.system.workspace import Workspace
+from webkitpy.common.system.executive_mock import MockExecutive
+
+
+class WorkspaceTest(unittest.TestCase):
+
+    def test_find_unused_filename(self):
+        filesystem = MockFileSystem({
+            "dir/foo.jpg": "",
+            "dir/foo-1.jpg": "",
+            "dir/foo-2.jpg": "",
+        })
+        workspace = Workspace(filesystem, None)
+        self.assertEqual(workspace.find_unused_filename("bar", "bar", "bar"), "bar/bar.bar")
+        self.assertEqual(workspace.find_unused_filename("dir", "foo", "jpg", search_limit=1), None)
+        self.assertEqual(workspace.find_unused_filename("dir", "foo", "jpg", search_limit=2), None)
+        self.assertEqual(workspace.find_unused_filename("dir", "foo", "jpg"), "dir/foo-3.jpg")
+
+    def test_create_zip(self):
+        workspace = Workspace(None, MockExecutive(should_log=True))
+        expected_stderr = "MOCK run_command: ['zip', '-9', '-r', '/zip/path', '.'], cwd=/source/path\n"
+        class MockZipFile(object):
+            def __init__(self, path):
+                self.filename = path
+        archive = OutputCapture().assert_outputs(self, workspace.create_zip, ["/zip/path", "/source/path", MockZipFile], expected_stderr=expected_stderr)
+        self.assertEqual(archive.filename, "/zip/path")
+
+    def test_create_zip_exception(self):
+        workspace = Workspace(None, MockExecutive(should_log=True, should_throw=True))
+        expected_stderr = "MOCK run_command: ['zip', '-9', '-r', '/zip/path', '.'], cwd=/source/path\n"
+        class MockZipFile(object):
+            def __init__(self, path):
+                self.filename = path
+        archive = OutputCapture().assert_outputs(self, workspace.create_zip, ["/zip/path", "/source/path", MockZipFile], expected_stderr=expected_stderr)
+        self.assertEqual(archive, None)
diff --git a/Tools/Scripts/webkitpy/common/system/zip_mock.py b/Tools/Scripts/webkitpy/common/system/zip_mock.py
new file mode 100644
index 0000000..dcfaba7
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/zip_mock.py
@@ -0,0 +1,55 @@
+# 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:
+#
+# 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.
+
+from webkitpy.common.system.fileset import FileSetFileHandle
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+
+
+class MockZip(object):
+    """A mock zip file that can have new files inserted into it."""
+    def __init__(self, filesystem=None):
+        self._filesystem = filesystem or MockFileSystem()
+        self._files = {}
+
+    def __str__(self):
+        return "MockZip"
+
+    def insert(self, filename, content):
+        self._files[filename] = content
+
+    def namelist(self):
+        return self._files.keys()
+
+    def open(self, filename):
+        return FileSetFileHandle(self, filename)
+
+    def read(self, filename):
+        return self._files[filename]
+
+    def extract(self, filename, path):
+        full_path = self._filesystem.join(path, filename)
+        contents = self.open(filename).contents()
+        self._filesystem.write_text_file(full_path, contents)
+
+    def delete(self, filename):
+        self._files[filename] = None
diff --git a/Tools/Scripts/webkitpy/common/system/zipfileset.py b/Tools/Scripts/webkitpy/common/system/zipfileset.py
new file mode 100644
index 0000000..5cf3616
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/zipfileset.py
@@ -0,0 +1,71 @@
+# 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:
+#
+# 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.
+
+import urllib
+import zipfile
+
+from webkitpy.common.net.networktransaction import NetworkTransaction
+from webkitpy.common.system.fileset import FileSetFileHandle
+from webkitpy.common.system.filesystem import FileSystem
+
+
+class ZipFileSet(object):
+    """The set of files in a zip file that resides at a URL (local or remote)"""
+    def __init__(self, zip_url, filesystem=None, zip_factory=None):
+        self._zip_url = zip_url
+        self._temp_file = None
+        self._zip_file = None
+        self._filesystem = filesystem or FileSystem()
+        self._zip_factory = zip_factory or self._retrieve_zip_file
+
+    def _retrieve_zip_file(self, zip_url):
+        temp_file = NetworkTransaction().run(lambda: urllib.urlretrieve(zip_url)[0])
+        return (temp_file, zipfile.ZipFile(temp_file))
+
+    def _load(self):
+        if self._zip_file is None:
+            self._temp_file, self._zip_file = self._zip_factory(self._zip_url)
+
+    def open(self, filename):
+        self._load()
+        return FileSetFileHandle(self, filename, self._filesystem)
+
+    def close(self):
+        if self._temp_file:
+            self._filesystem.remove(self._temp_file)
+            self._temp_file = None
+
+    def namelist(self):
+        self._load()
+        return self._zip_file.namelist()
+
+    def read(self, filename):
+        self._load()
+        return self._zip_file.read(filename)
+
+    def extract(self, filename, path):
+        self._load()
+        self._zip_file.extract(filename, path)
+
+    def delete(self, filename):
+        raise Exception("Can't delete from a ZipFileSet.")
diff --git a/Tools/Scripts/webkitpy/common/system/zipfileset_mock.py b/Tools/Scripts/webkitpy/common/system/zipfileset_mock.py
new file mode 100644
index 0000000..24ac8cb
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/zipfileset_mock.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2011 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.
+
+
+def make_factory(ziphashes):
+    """ZipFileSet factory routine that looks up zipfiles in a dict;
+    each zipfile should also be a dict of member names -> contents."""
+    class MockZipFileSet(object):
+        def __init__(self, url):
+            self._url = url
+            self._ziphash = ziphashes[url]
+
+        def namelist(self):
+            return self._ziphash.keys()
+
+        def read(self, member):
+            return self._ziphash[member]
+
+        def close(self):
+            pass
+
+    def maker(url):
+        # We return None because there's no tempfile to delete.
+        return (None, MockZipFileSet(url))
+
+    return maker
diff --git a/Tools/Scripts/webkitpy/common/system/zipfileset_unittest.py b/Tools/Scripts/webkitpy/common/system/zipfileset_unittest.py
new file mode 100644
index 0000000..16a74cb
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/system/zipfileset_unittest.py
@@ -0,0 +1,98 @@
+# 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:
+#
+# 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.
+
+import shutil
+import tempfile
+import unittest
+import zipfile
+
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.zipfileset import ZipFileSet
+
+
+class FakeZip(object):
+    def __init__(self, filesystem):
+        self._filesystem = filesystem
+        self._files = {}
+
+    def add_file(self, filename, contents):
+        self._files[filename] = contents
+
+    def open(self, filename):
+        return FileSetFileHandle(self, filename, self._filesystem)
+
+    def namelist(self):
+        return self._files.keys()
+
+    def read(self, filename):
+        return self._files[filename]
+
+    def extract(self, filename, path):
+        self._filesystem.write_text_file(self._filesystem.join(path, filename), self.read(filename))
+
+    def delete(self, filename):
+        raise Exception("Can't delete from a ZipFileSet.")
+
+
+class ZipFileSetTest(unittest.TestCase):
+    def setUp(self):
+        self._filesystem = MockFileSystem()
+        self._zip = ZipFileSet('blah', self._filesystem, self.make_fake_zip)
+
+    def make_fake_zip(self, zip_url):
+        result = FakeZip(self._filesystem)
+        result.add_file('some-file', 'contents')
+        result.add_file('a/b/some-other-file', 'other contents')
+        return (None, result)
+
+    def test_open(self):
+        file = self._zip.open('a/b/some-other-file')
+        self.assertEquals('a/b/some-other-file', file.name())
+        self.assertEquals('other contents', file.contents())
+
+    def test_close(self):
+        zipfileset = ZipFileSet('blah', self._filesystem, self.make_fake_zip)
+        zipfileset.close()
+
+    def test_read(self):
+        self.assertEquals('contents', self._zip.read('some-file'))
+
+    def test_extract(self):
+        self._filesystem.maybe_make_directory('/some-dir')
+        self._zip.extract('some-file', '/some-dir')
+        self.assertTrue(self._filesystem.isfile('/some-dir/some-file'))
+
+    def test_deep_extract(self):
+        self._filesystem.maybe_make_directory('/some-dir')
+        self._zip.extract('a/b/some-other-file', '/some-dir')
+        self.assertTrue(self._filesystem.isfile('/some-dir/a/b/some-other-file'))
+
+    def test_cant_delete(self):
+        self.assertRaises(Exception, self._zip.delete, 'some-file')
+
+    def test_namelist(self):
+        self.assertTrue('some-file' in self._zip.namelist())
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/common/thread/__init__.py b/Tools/Scripts/webkitpy/common/thread/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/thread/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/common/thread/messagepump.py b/Tools/Scripts/webkitpy/common/thread/messagepump.py
new file mode 100644
index 0000000..0e39285
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/thread/messagepump.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.
+
+
+class MessagePumpDelegate(object):
+    def schedule(self, interval, callback):
+        raise NotImplementedError, "subclasses must implement"
+
+    def message_available(self, message):
+        raise NotImplementedError, "subclasses must implement"
+
+    def final_message_delivered(self):
+        raise NotImplementedError, "subclasses must implement"
+
+
+class MessagePump(object):
+    interval = 10 # seconds
+
+    def __init__(self, delegate, message_queue):
+        self._delegate = delegate
+        self._message_queue = message_queue
+        self._schedule()
+
+    def _schedule(self):
+        self._delegate.schedule(self.interval, self._callback)
+
+    def _callback(self):
+        (messages, is_running) = self._message_queue.take_all()
+        for message in messages:
+            self._delegate.message_available(message)
+        if not is_running:
+            self._delegate.final_message_delivered()
+            return
+        self._schedule()
diff --git a/Tools/Scripts/webkitpy/common/thread/messagepump_unittest.py b/Tools/Scripts/webkitpy/common/thread/messagepump_unittest.py
new file mode 100644
index 0000000..f731db2
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/thread/messagepump_unittest.py
@@ -0,0 +1,83 @@
+# 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.thread.messagepump import MessagePump, MessagePumpDelegate
+from webkitpy.common.thread.threadedmessagequeue import ThreadedMessageQueue
+
+
+class TestDelegate(MessagePumpDelegate):
+    def __init__(self):
+        self.log = []
+
+    def schedule(self, interval, callback):
+        self.callback = callback
+        self.log.append("schedule")
+
+    def message_available(self, message):
+        self.log.append("message_available: %s" % message)
+
+    def final_message_delivered(self):
+        self.log.append("final_message_delivered")
+
+
+class MessagePumpTest(unittest.TestCase):
+
+    def test_basic(self):
+        queue = ThreadedMessageQueue()
+        delegate = TestDelegate()
+        pump = MessagePump(delegate, queue)
+        self.assertEqual(delegate.log, [
+            'schedule'
+        ])
+        delegate.callback()
+        queue.post("Hello")
+        queue.post("There")
+        delegate.callback()
+        self.assertEqual(delegate.log, [
+            'schedule',
+            'schedule',
+            'message_available: Hello',
+            'message_available: There',
+            'schedule'
+        ])
+        queue.post("More")
+        queue.post("Messages")
+        queue.stop()
+        delegate.callback()
+        self.assertEqual(delegate.log, [
+            'schedule',
+            'schedule',
+            'message_available: Hello',
+            'message_available: There',
+            'schedule',
+            'message_available: More',
+            'message_available: Messages',
+            'final_message_delivered'
+        ])
diff --git a/Tools/Scripts/webkitpy/common/thread/threadedmessagequeue.py b/Tools/Scripts/webkitpy/common/thread/threadedmessagequeue.py
new file mode 100644
index 0000000..e434767
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/thread/threadedmessagequeue.py
@@ -0,0 +1,52 @@
+# 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 threading
+
+
+class ThreadedMessageQueue(object):
+    def __init__(self):
+        self._messages = []
+        self._is_running = True
+        self._lock = threading.Lock()
+
+    def post(self, message):
+        with self._lock:
+            self._messages.append(message)
+
+    def stop(self):
+        with self._lock:
+            self._is_running = False
+
+    def take_all(self):
+        with self._lock:
+            messages = self._messages
+            is_running = self._is_running
+            self._messages = []
+        return (messages, is_running)
+
diff --git a/Tools/Scripts/webkitpy/common/thread/threadedmessagequeue_unittest.py b/Tools/Scripts/webkitpy/common/thread/threadedmessagequeue_unittest.py
new file mode 100644
index 0000000..cb67c1e
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/thread/threadedmessagequeue_unittest.py
@@ -0,0 +1,53 @@
+# 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.thread.threadedmessagequeue import ThreadedMessageQueue
+
+class ThreadedMessageQueueTest(unittest.TestCase):
+
+    def test_basic(self):
+        queue = ThreadedMessageQueue()
+        queue.post("Hello")
+        queue.post("There")
+        (messages, is_running) = queue.take_all()
+        self.assertEqual(messages, ["Hello", "There"])
+        self.assertTrue(is_running)
+        (messages, is_running) = queue.take_all()
+        self.assertEqual(messages, [])
+        self.assertTrue(is_running)
+        queue.post("More")
+        queue.stop()
+        queue.post("Messages")
+        (messages, is_running) = queue.take_all()
+        self.assertEqual(messages, ["More", "Messages"])
+        self.assertFalse(is_running)
+        (messages, is_running) = queue.take_all()
+        self.assertEqual(messages, [])
+        self.assertFalse(is_running)
diff --git a/Tools/Scripts/webkitpy/common/version_check.py b/Tools/Scripts/webkitpy/common/version_check.py
new file mode 100644
index 0000000..6acc9b4
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/version_check.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+# Copyright (c) 2011 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 sys
+
+if sys.version < '2.6' or sys.version >= '2.8':
+    print >> sys.stderr, "Unsupported Python version: WebKit only supports 2.6.x - 2.7.x, and you're running %s." % sys.version.split()[0]
+    sys.exit(1)
diff --git a/Tools/Scripts/webkitpy/common/watchlist/__init__.py b/Tools/Scripts/webkitpy/common/watchlist/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/watchlist/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/common/watchlist/amountchangedpattern.py b/Tools/Scripts/webkitpy/common/watchlist/amountchangedpattern.py
new file mode 100644
index 0000000..fc8adc9
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/watchlist/amountchangedpattern.py
@@ -0,0 +1,70 @@
+# Copyright (C) 2011 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 AmountChangedPattern:
+    def __init__(self, compile_regex, index_for_zero_value):
+        self._regex = compile_regex
+        self._index_for_zero_value = index_for_zero_value
+
+    def match(self, path, diff_file):
+        examined_strings = set()
+        for diff_line in diff_file:
+            if diff_line[self._index_for_zero_value]:
+                continue
+            match = self._regex.search(diff_line[2])
+            if not match:
+                continue
+            matching_string = match.group(0)
+            if matching_string in examined_strings:
+                continue
+            if self._instance_difference(diff_file, matching_string) > 0:
+                return True
+            # Avoid reprocessing this same string.
+            examined_strings.add(matching_string)
+        return False
+
+    def _instance_difference(self, diff_file, matching_string):
+        '''Returns the difference between the number of string occurences in
+        the added lines and deleted lines (which one is subtracted from the
+        other depends on _index_for_zero_value).'''
+        count = 0
+        for diff_line in diff_file:
+            # If the line is unchanged, then don't examine it.
+            if diff_line[self._index_for_zero_value] and diff_line[1 - self._index_for_zero_value]:
+                continue
+            location_found = -len(matching_string)
+            while True:
+                location_found = diff_line[2].find(matching_string, location_found + len(matching_string))
+                if location_found == -1:
+                    break
+                if not diff_line[self._index_for_zero_value]:
+                    count += 1
+                else:
+                    count -= 1
+        return count
diff --git a/Tools/Scripts/webkitpy/common/watchlist/amountchangedpattern_unittest.py b/Tools/Scripts/webkitpy/common/watchlist/amountchangedpattern_unittest.py
new file mode 100644
index 0000000..7ae45fa
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/watchlist/amountchangedpattern_unittest.py
@@ -0,0 +1,70 @@
+# Copyright (C) 2011 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.
+
+
+'''Unit tests for amountchangedpattern.py.'''
+
+
+import re
+import unittest
+
+
+from webkitpy.common.watchlist.amountchangedpattern import AmountChangedPattern
+
+
+class AmountChangedPatternTest(unittest.TestCase):
+
+    # A quick note about the diff file structure.
+    # The first column indicated the old line number.
+    # The second column indicates the new line number.
+    # 0 in either column indicates it had no old or new line number.
+    _DIFF_FILE = ((0, 1, 'hi hi'),
+                  (1, 0, 'bye hi'),
+                  (2, 2, 'other hi'),
+                  (3, 0, 'both'),
+                  (0, 3, 'both'),
+                  )
+
+    def run_amount_changed_pattern_match(self, pattern, index_for_zero_value):
+        return AmountChangedPattern(re.compile(pattern), index_for_zero_value).match(None, self._DIFF_FILE)
+
+    def test_added_lines(self):
+        self.assertTrue(self.run_amount_changed_pattern_match('hi', 0))
+        self.assertTrue(self.run_amount_changed_pattern_match('hi hi', 0))
+        self.assertFalse(self.run_amount_changed_pattern_match('other', 0))
+        self.assertFalse(self.run_amount_changed_pattern_match('both', 0))
+        self.assertFalse(self.run_amount_changed_pattern_match('bye', 0))
+        self.assertFalse(self.run_amount_changed_pattern_match('MatchesNothing', 0))
+
+    def test_removed_lines(self):
+        self.assertFalse(self.run_amount_changed_pattern_match('hi', 1))
+        self.assertFalse(self.run_amount_changed_pattern_match('hi hi', 1))
+        self.assertFalse(self.run_amount_changed_pattern_match('other', 1))
+        self.assertFalse(self.run_amount_changed_pattern_match('both', 1))
+        self.assertTrue(self.run_amount_changed_pattern_match('bye', 1))
+        self.assertFalse(self.run_amount_changed_pattern_match('MatchesNothing', 1))
diff --git a/Tools/Scripts/webkitpy/common/watchlist/changedlinepattern.py b/Tools/Scripts/webkitpy/common/watchlist/changedlinepattern.py
new file mode 100644
index 0000000..61fac9a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/watchlist/changedlinepattern.py
@@ -0,0 +1,41 @@
+# Copyright (C) 2011 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 ChangedLinePattern:
+    def __init__(self, compile_regex, index_for_zero_value):
+        self._regex = compile_regex
+        self._index_for_zero_value = index_for_zero_value
+
+    def match(self, path, diff_file):
+        for diff_line in diff_file:
+            if diff_line[self._index_for_zero_value]:
+                continue
+            if self._regex.search(diff_line[2]):
+                return True
+        return False
diff --git a/Tools/Scripts/webkitpy/common/watchlist/changedlinepattern_unittest.py b/Tools/Scripts/webkitpy/common/watchlist/changedlinepattern_unittest.py
new file mode 100644
index 0000000..1f2aeda
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/watchlist/changedlinepattern_unittest.py
@@ -0,0 +1,68 @@
+# Copyright (C) 2011 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.
+
+'''Unit tests for changedlinepattern.py.'''
+
+import re
+import unittest
+
+
+from webkitpy.common.watchlist.changedlinepattern import ChangedLinePattern
+
+
+class ChangedLinePatternTest(unittest.TestCase):
+
+    # A quick note about the diff file structure.
+    # The first column indicated the old line number.
+    # The second column indicates the new line number.
+    # 0 in either column indicates it had no old or new line number.
+    _DIFF_FILE = ((0, 1, 'hi'),
+                  (1, 0, 'bye'),
+                  (2, 2, 'other'),
+                  (3, 0, 'both'),
+                  (0, 3, 'both'),
+                  )
+
+    def run_changed_line_pattern_match(self, pattern, index_for_zero_value):
+        return ChangedLinePattern(re.compile(pattern), index_for_zero_value).match(None, self._DIFF_FILE)
+
+    def test_added_lines(self):
+        self.assertTrue(self.run_changed_line_pattern_match('hi', 0))
+        self.assertTrue(self.run_changed_line_pattern_match('h.', 0))
+        self.assertTrue(self.run_changed_line_pattern_match('both', 0))
+        self.assertFalse(self.run_changed_line_pattern_match('bye', 0))
+        self.assertFalse(self.run_changed_line_pattern_match('y', 0))
+        self.assertFalse(self.run_changed_line_pattern_match('other', 0))
+
+    def test_removed_lines(self):
+        self.assertFalse(self.run_changed_line_pattern_match('hi', 1))
+        self.assertFalse(self.run_changed_line_pattern_match('h.', 1))
+        self.assertTrue(self.run_changed_line_pattern_match('both', 1))
+        self.assertTrue(self.run_changed_line_pattern_match('bye', 1))
+        self.assertTrue(self.run_changed_line_pattern_match('y', 1))
+        self.assertFalse(self.run_changed_line_pattern_match('other', 1))
diff --git a/Tools/Scripts/webkitpy/common/watchlist/filenamepattern.py b/Tools/Scripts/webkitpy/common/watchlist/filenamepattern.py
new file mode 100644
index 0000000..799eeb4
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/watchlist/filenamepattern.py
@@ -0,0 +1,35 @@
+# Copyright (C) 2011 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 FilenamePattern:
+    def __init__(self, compiled_regex):
+        self._regex = compiled_regex
+
+    def match(self, path, diff_file):
+        return self._regex.match(path)
diff --git a/Tools/Scripts/webkitpy/common/watchlist/filenamepattern_unittest.py b/Tools/Scripts/webkitpy/common/watchlist/filenamepattern_unittest.py
new file mode 100644
index 0000000..0afdf30
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/watchlist/filenamepattern_unittest.py
@@ -0,0 +1,52 @@
+# Copyright (C) 2011 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 re
+import unittest
+
+
+from webkitpy.common.watchlist.filenamepattern import FilenamePattern
+
+
+class FileNamePatternTest(unittest.TestCase):
+    def test_filename_pattern_literal(self):
+        filename_pattern = FilenamePattern(re.compile(r'MyFileName\.cpp'))
+
+        # Note the follow filenames are not regex.
+        self.assertTrue(filename_pattern.match('MyFileName.cpp', None))
+        self.assertTrue(filename_pattern.match('MyFileName.cppa', None))
+        self.assertFalse(filename_pattern.match('aMyFileName.cpp', None))
+        self.assertFalse(filename_pattern.match('MyFileNamebcpp', None))
+
+    def test_filename_pattern_substring(self):
+        filename_pattern = FilenamePattern(re.compile(r'.*\\MyFileName\..*'))
+
+        # Note the follow filenames are not regex.
+        self.assertTrue(filename_pattern.match(r'\\MyFileName.cpp', None))
+        self.assertTrue(filename_pattern.match(r'a\\MyFileName.h', None))
+        self.assertFalse(filename_pattern.match(r'\\aMyFileName.cpp', None))
diff --git a/Tools/Scripts/webkitpy/common/watchlist/watchlist.py b/Tools/Scripts/webkitpy/common/watchlist/watchlist.py
new file mode 100644
index 0000000..4a81039
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/watchlist/watchlist.py
@@ -0,0 +1,75 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.checkout.diff_parser import DiffParser
+
+
+class WatchList(object):
+    def __init__(self):
+        self.definitions = {}
+        self.cc_rules = set()
+        self.message_rules = set()
+
+    def find_matching_definitions(self, diff):
+        matching_definitions = set()
+        patch_files = DiffParser(diff.splitlines()).files
+
+        for path, diff_file in patch_files.iteritems():
+            for definition in self.definitions:
+                # If a definition has already matched, there is no need to process it.
+                if definition in matching_definitions:
+                    continue
+
+                # See if the definition matches within one file.
+                for pattern in self.definitions[definition]:
+                    if not pattern.match(path, diff_file.lines):
+                        break
+                else:
+                    matching_definitions.add(definition)
+        return matching_definitions
+
+    def _determine_instructions(self, matching_definitions, rules):
+        instructions = set()
+        for rule in rules:
+            if rule.match(matching_definitions):
+                instructions.update(rule.instructions())
+        # Sort the results to make the order deterministic (for consistency and easier testing).
+        return sorted(instructions)
+
+    def determine_cc_list(self, matching_definitions):
+        return self._determine_instructions(matching_definitions, self.cc_rules)
+
+    def determine_messages(self, matching_definitions):
+        return self._determine_instructions(matching_definitions, self.message_rules)
+
+    def determine_cc_and_messages(self, diff):
+        definitions = self.find_matching_definitions(diff)
+        return {
+            'cc_list': self.determine_cc_list(definitions),
+            'messages':  self.determine_messages(definitions),
+        }
diff --git a/Tools/Scripts/webkitpy/common/watchlist/watchlist_mock.py b/Tools/Scripts/webkitpy/common/watchlist/watchlist_mock.py
new file mode 100644
index 0000000..2fd2f88
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/watchlist/watchlist_mock.py
@@ -0,0 +1,35 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.system.deprecated_logging import log
+
+
+class MockWatchList(object):
+    def determine_cc_and_messages(self, diff):
+        log("MockWatchList: determine_cc_and_messages")
+        return {'cc_list': ['abarth@webkit.org', 'eric@webkit.org', 'levin@chromium.org'], 'messages': ['Message1.', 'Message2.'], }
diff --git a/Tools/Scripts/webkitpy/common/watchlist/watchlist_unittest.py b/Tools/Scripts/webkitpy/common/watchlist/watchlist_unittest.py
new file mode 100644
index 0000000..09010b2
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/watchlist/watchlist_unittest.py
@@ -0,0 +1,277 @@
+# Copyright (C) 2011 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.
+
+'''Unit tests for watchlist.py.'''
+
+import unittest
+
+from webkitpy.common.checkout.diff_test_data import DIFF_TEST_DATA
+from webkitpy.common.watchlist.watchlistparser import WatchListParser
+
+
+class WatchListTest(unittest.TestCase):
+    def setUp(self):
+        self._watch_list_parser = WatchListParser()
+
+    def test_filename_definition_no_matches(self):
+        watch_list = self._watch_list_parser.parse(
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "filename": r".*\\MyFileName\\.cpp",'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList1": ['
+            '            "levin@chromium.org",'
+            '        ],'
+           '    },'
+            '}')
+        self.assertEquals(set([]), watch_list.find_matching_definitions(DIFF_TEST_DATA))
+
+    def test_filename_definition(self):
+        watch_list = self._watch_list_parser.parse(
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "filename": r"WebCore/rendering/style/StyleFlexibleBoxData\.h",'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList1": ['
+            '            "levin@chromium.org",'
+            '        ],'
+           '    },'
+            '}')
+        self.assertEquals(set(['WatchList1']), watch_list.find_matching_definitions(DIFF_TEST_DATA))
+
+    def test_cc_rules_simple(self):
+        watch_list = self._watch_list_parser.parse(
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "filename": r"WebCore/rendering/style/StyleFlexibleBoxData\.h",'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList1": ['
+            '            "levin@chromium.org",'
+            '        ],'
+           '    },'
+            '}')
+        cc_and_messages = watch_list.determine_cc_and_messages(DIFF_TEST_DATA)
+        self.assertEquals({
+                'cc_list': ['levin@chromium.org'],
+                'messages': [],
+                }, cc_and_messages)
+
+    def test_cc_rules_complex(self):
+        watch_list = self._watch_list_parser.parse(
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "filename": r"WebCore/rendering/style/StyleFlexibleBoxData\.h",'
+            '        },'
+            '        "WatchList2": {'
+            '            "filename": r"WillNotMatch",'
+            '        },'
+            '        "WatchList3": {'
+            '            "filename": r"WillNotMatch",'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList2|WatchList1|WatchList3": [ "levin@chromium.org", ],'
+            '    },'
+            '}')
+        cc_and_messages = watch_list.determine_cc_and_messages(DIFF_TEST_DATA)
+        self.assertEquals({
+                'cc_list': ['levin@chromium.org'],
+                'messages': [],
+                }, cc_and_messages)
+
+    def test_cc_and_message_rules_complex(self):
+        watch_list = self._watch_list_parser.parse(
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "filename": r"WebCore/rendering/style/StyleFlexibleBoxData\.h",'
+            '        },'
+            '        "WatchList2": {'
+            '            "filename": r"WillNotMatch",'
+            '        },'
+            '        "WatchList3": {'
+            '            "filename": r"WillNotMatch",'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList2|WatchList1|WatchList3": [ "levin@chromium.org", ],'
+            '    },'
+            '    "MESSAGE_RULES": {'
+            '        "WatchList2|WatchList1|WatchList3": [ "msg1", "msg2", ],'
+            '    },'
+            '}')
+        cc_and_messages = watch_list.determine_cc_and_messages(DIFF_TEST_DATA)
+        self.assertEquals({
+                'cc_list': ['levin@chromium.org'],
+                'messages': ['msg1', 'msg2'],
+                }, cc_and_messages)
+
+    def test_cc_and_message_rules_no_matches(self):
+        watch_list = self._watch_list_parser.parse(
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "filename": r"WebCore/rendering/style/ThisFileDoesNotExist\.h",'
+            '        },'
+            '        "WatchList2": {'
+            '            "filename": r"WillNotMatch",'
+            '        },'
+            '        "WatchList3": {'
+            '            "filename": r"WillNotMatch",'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList2|WatchList1|WatchList3": [ "levin@chromium.org", ],'
+            '    },'
+            '    "MESSAGE_RULES": {'
+            '        "WatchList2|WatchList1|WatchList3": [ "msg1", "msg2", ],'
+            '    },'
+            '}')
+        cc_and_messages = watch_list.determine_cc_and_messages(DIFF_TEST_DATA)
+        self.assertEquals({
+                'cc_list': [],
+                'messages': [],
+                }, cc_and_messages)
+
+    def test_added_match(self):
+        watch_list = self._watch_list_parser.parse(
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "in_added_lines": r"RenderStyle::initialBoxOrient",'
+            '        },'
+            '        "WatchList2": {'
+            '            "in_deleted_lines": r"RenderStyle::initialBoxOrient",'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList1": [ "eric@webkit.org", ],'
+            '        "WatchList2": [ "abarth@webkit.org", ],'
+            '    },'
+            '}')
+        cc_and_messages = watch_list.determine_cc_and_messages(DIFF_TEST_DATA)
+        self.assertEquals({
+                'cc_list': ['eric@webkit.org'],
+                'messages': [],
+                }, cc_and_messages)
+
+    def test_deleted_match(self):
+        watch_list = self._watch_list_parser.parse(
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "in_added_lines": r"unsigned orient: 1;",'
+            '        },'
+            '        "WatchList2": {'
+            '            "in_deleted_lines": r"unsigned orient: 1;",'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList1": [ "eric@webkit.org", ],'
+            '        "WatchList2": [ "abarth@webkit.org", ],'
+            '    },'
+            '}')
+        cc_and_messages = watch_list.determine_cc_and_messages(DIFF_TEST_DATA)
+        self.assertEquals({
+                'cc_list': ['abarth@webkit.org'],
+                'messages': [],
+                }, cc_and_messages)
+
+    def test_more_and_less_match(self):
+        watch_list = self._watch_list_parser.parse(
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            # This pattern is in both added and deleted lines, so no match.
+            '            "more": r"userSelect == o\.userSelect",'
+            '        },'
+            '        "WatchList2": {'
+            '            "more": r"boxOrient\(o\.boxOrient\)",'
+            '        },'
+            '        "WatchList3": {'
+            '            "less": r"unsigned orient"'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList1": [ "eric@webkit.org", ],'
+            '        "WatchList2": [ "levin@chromium.org", ],'
+            '    },'
+            '    "MESSAGE_RULES": {'
+            '        "WatchList3": ["Test message."],'
+            '    },'
+            '}')
+        cc_and_messages = watch_list.determine_cc_and_messages(DIFF_TEST_DATA)
+        self.assertEquals({
+                'cc_list': ['levin@chromium.org'],
+                'messages': ["Test message."],
+                }, cc_and_messages)
+
+    def test_complex_match(self):
+        watch_list = self._watch_list_parser.parse(
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "filename": r"WebCore/rendering/style/StyleRareInheritedData\.cpp",'
+            '            "in_added_lines": r"\&\& boxOrient == o\.boxOrient;",'
+            '            "in_deleted_lines": r"\&\& userSelect == o\.userSelect;",'
+            '            "more": r"boxOrient\(o\.boxOrient\)",'
+            '        },'
+            '        "WatchList2": {'
+            '            "filename": r"WebCore/rendering/style/StyleRareInheritedData\.cpp",'
+            '            "in_added_lines": r"RenderStyle::initialBoxOrient",'
+            '            "less": r"userSelect;"'
+            '        },'
+            # WatchList3 won't match because these two patterns aren't in the same file.
+            '        "WatchList3": {'
+            '            "in_added_lines": r"RenderStyle::initialBoxOrient",'
+            '            "in_deleted_lines": r"unsigned orient: 1;",'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList1": [ "eric@webkit.org", ],'
+            '        "WatchList3": [ "abarth@webkit.org", ],'
+            '    },'
+            '    "MESSAGE_RULES": {'
+            '        "WatchList2": ["This is a test message."],'
+            '    },'
+            '}')
+        cc_and_messages = watch_list.determine_cc_and_messages(DIFF_TEST_DATA)
+        self.assertEquals({
+                'cc_list': ['eric@webkit.org'],
+                'messages': ["This is a test message."],
+                }, cc_and_messages)
diff --git a/Tools/Scripts/webkitpy/common/watchlist/watchlistloader.py b/Tools/Scripts/webkitpy/common/watchlist/watchlistloader.py
new file mode 100644
index 0000000..aa816e3
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/watchlist/watchlistloader.py
@@ -0,0 +1,43 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.watchlist.watchlistparser import WatchListParser
+
+
+class WatchListLoader(object):
+    def __init__(self, filesystem):
+        self._filesystem = filesystem
+
+    def load(self):
+        config_path = self._filesystem.dirname(self._filesystem.path_to_module('webkitpy.common.config'))
+        watch_list_full_path = self._filesystem.join(config_path, 'watchlist')
+        if not self._filesystem.exists(watch_list_full_path):
+            raise Exception('Watch list file (%s) not found.' % watch_list_full_path)
+
+        watch_list_contents = self._filesystem.read_text_file(watch_list_full_path)
+        return WatchListParser().parse(watch_list_contents)
diff --git a/Tools/Scripts/webkitpy/common/watchlist/watchlistloader_unittest.py b/Tools/Scripts/webkitpy/common/watchlist/watchlistloader_unittest.py
new file mode 100644
index 0000000..8d3fa98
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/watchlist/watchlistloader_unittest.py
@@ -0,0 +1,45 @@
+# Copyright (C) 2011 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.
+
+'''Unit tests for watchlistloader.py.'''
+
+from webkitpy.common import webkitunittest
+from webkitpy.common.system import filesystem_mock
+from webkitpy.common.system import filesystem
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.watchlist.watchlistloader import WatchListLoader
+
+
+class WatchListLoaderTest(webkitunittest.TestCase):
+    def test_watch_list_not_found(self):
+        loader = WatchListLoader(filesystem_mock.MockFileSystem())
+        self.assertRaisesRegexp(Exception, r'Watch list file \(.*/watchlist\) not found\.', loader.load)
+
+    def test_watch_list_load(self):
+        # Test parsing of the checked-in watch list.
+        OutputCapture().assert_outputs(self, WatchListLoader(filesystem.FileSystem()).load, expected_logs="")
diff --git a/Tools/Scripts/webkitpy/common/watchlist/watchlistparser.py b/Tools/Scripts/webkitpy/common/watchlist/watchlistparser.py
new file mode 100644
index 0000000..c72eab3
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/watchlist/watchlistparser.py
@@ -0,0 +1,181 @@
+# Copyright (C) 2011 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 difflib
+import logging
+import re
+
+from webkitpy.common.watchlist.amountchangedpattern import AmountChangedPattern
+from webkitpy.common.watchlist.changedlinepattern import ChangedLinePattern
+from webkitpy.common.watchlist.filenamepattern import FilenamePattern
+from webkitpy.common.watchlist.watchlist import WatchList
+from webkitpy.common.watchlist.watchlistrule import WatchListRule
+from webkitpy.common.config.committers import CommitterList
+
+
+_log = logging.getLogger(__name__)
+
+
+class WatchListParser(object):
+    _DEFINITIONS = 'DEFINITIONS'
+    _CC_RULES = 'CC_RULES'
+    _MESSAGE_RULES = 'MESSAGE_RULES'
+    _INVALID_DEFINITION_NAME_REGEX = r'\|'
+
+    def __init__(self, log_error=None):
+        self._log_error = log_error or _log.error
+        self._section_parsers = {
+            self._DEFINITIONS: self._parse_definition_section,
+            self._CC_RULES: self._parse_cc_rules,
+            self._MESSAGE_RULES: self._parse_message_rules,
+        }
+        self._definition_pattern_parsers = {
+            'filename': FilenamePattern,
+            'in_added_lines': (lambda compiled_regex: ChangedLinePattern(compiled_regex, 0)),
+            'in_deleted_lines': (lambda compiled_regex: ChangedLinePattern(compiled_regex, 1)),
+            'less': (lambda compiled_regex: AmountChangedPattern(compiled_regex, 1)),
+            'more': (lambda compiled_regex: AmountChangedPattern(compiled_regex, 0)),
+        }
+
+    def parse(self, watch_list_contents):
+        watch_list = WatchList()
+
+        # Change the watch list text into a dictionary.
+        dictionary = self._eval_watch_list(watch_list_contents)
+
+        # Parse the top level sections in the watch list.
+        for section in dictionary:
+            parser = self._section_parsers.get(section)
+            if not parser:
+                self._log_error(('Unknown section "%s" in watch list.'
+                                + self._suggest_words(section, self._section_parsers.keys()))
+                               % section)
+                continue
+            parser(dictionary[section], watch_list)
+
+        self._validate(watch_list)
+        return watch_list
+
+    def _eval_watch_list(self, watch_list_contents):
+        return eval(watch_list_contents, {'__builtins__': None}, None)
+
+    def _suggest_words(self, invalid_word, valid_words):
+        close_matches = difflib.get_close_matches(invalid_word, valid_words)
+        if not close_matches:
+            return ''
+        return '\n\nPerhaps it should be %s.' % (' or '.join(close_matches))
+
+    def _parse_definition_section(self, definition_section, watch_list):
+        definitions = {}
+        for name in definition_section:
+            invalid_character = re.search(self._INVALID_DEFINITION_NAME_REGEX, name)
+            if invalid_character:
+                self._log_error('Invalid character "%s" in definition "%s".' % (invalid_character.group(0), name))
+                continue
+
+            definition = definition_section[name]
+            definitions[name] = []
+            for pattern_type in definition:
+                pattern_parser = self._definition_pattern_parsers.get(pattern_type)
+                if not pattern_parser:
+                    self._log_error(('Unknown pattern type "%s" in definition "%s".'
+                                     + self._suggest_words(pattern_type, self._definition_pattern_parsers.keys()))
+                                    % (pattern_type, name))
+                    continue
+
+                try:
+                    compiled_regex = re.compile(definition[pattern_type])
+                except Exception, e:
+                    self._log_error('The regex "%s" is invalid due to "%s".' % (definition[pattern_type], str(e)))
+                    continue
+
+                pattern = pattern_parser(compiled_regex)
+                definitions[name].append(pattern)
+            if not definitions[name]:
+                self._log_error('The definition "%s" has no patterns, so it should be deleted.' % name)
+                continue
+        watch_list.definitions = definitions
+
+    def _parse_rules(self, rules_section):
+        rules = []
+        for complex_definition in rules_section:
+            instructions = rules_section[complex_definition]
+            if not instructions:
+                self._log_error('A rule for definition "%s" is empty, so it should be deleted.' % complex_definition)
+                continue
+            rules.append(WatchListRule(complex_definition, instructions))
+        return rules
+
+    def _parse_cc_rules(self, cc_section, watch_list):
+        watch_list.cc_rules = self._parse_rules(cc_section)
+
+    def _parse_message_rules(self, message_section, watch_list):
+        watch_list.message_rules = self._parse_rules(message_section)
+
+    def _validate(self, watch_list):
+        cc_definitions_set = self._rule_definitions_as_set(watch_list.cc_rules)
+        messages_definitions_set = self._rule_definitions_as_set(watch_list.message_rules)
+        self._verify_all_definitions_are_used(watch_list, cc_definitions_set.union(messages_definitions_set))
+
+        self._validate_definitions(cc_definitions_set, self._CC_RULES, watch_list)
+        self._validate_definitions(messages_definitions_set, self._MESSAGE_RULES, watch_list)
+
+        accounts = CommitterList()
+        for cc_rule in watch_list.cc_rules:
+            # Copy the instructions since we'll be remove items from the original list and
+            # modifying a list while iterating through it leads to undefined behavior.
+            intructions_copy = cc_rule.instructions()[:]
+            for email in intructions_copy:
+                if not accounts.account_by_login(email):
+                    cc_rule.remove_instruction(email)
+                    self._log_error("The email alias %s which is in the watchlist is not listed as a contributor in committers.py" % email)
+                    continue
+
+    def _verify_all_definitions_are_used(self, watch_list, used_definitions):
+        definitions_not_used = set(watch_list.definitions.keys())
+        definitions_not_used.difference_update(used_definitions)
+        if definitions_not_used:
+            self._log_error('The following definitions are not used and should be removed: %s' % (', '.join(definitions_not_used)))
+
+    def _validate_definitions(self, definitions, rules_section_name, watch_list):
+        declared_definitions = watch_list.definitions.keys()
+        definition_set = set(definitions)
+        definition_set.difference_update(declared_definitions)
+
+        if definition_set:
+            suggestions = ''
+            if len(definition_set) == 1:
+                suggestions = self._suggest_words(set().union(definition_set).pop(), declared_definitions)
+            self._log_error('In section "%s", the following definitions are not used and should be removed: %s%s' % (rules_section_name, ', '.join(definition_set), suggestions))
+
+    def _rule_definitions_as_set(self, rules):
+        definition_set = set()
+        for rule in rules:
+            definition_set = definition_set.union(rule.definitions_to_match)
+        return definition_set
diff --git a/Tools/Scripts/webkitpy/common/watchlist/watchlistparser_unittest.py b/Tools/Scripts/webkitpy/common/watchlist/watchlistparser_unittest.py
new file mode 100644
index 0000000..3bd4dc2
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/watchlist/watchlistparser_unittest.py
@@ -0,0 +1,259 @@
+# Copyright (C) 2011 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.
+
+'''Unit tests for watchlistparser.py.'''
+
+
+import logging
+import sys
+
+
+from webkitpy.common import webkitunittest
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.watchlist.watchlistparser import WatchListParser
+
+
+class WatchListParserTest(webkitunittest.TestCase):
+    def setUp(self):
+        webkitunittest.TestCase.setUp(self)
+        self._watch_list_parser = WatchListParser()
+
+    def test_bad_section(self):
+        watch_list = ('{"FOO": {}}')
+        OutputCapture().assert_outputs(self, self._watch_list_parser.parse, args=[watch_list],
+                                       expected_logs='Unknown section "FOO" in watch list.\n')
+
+    def test_section_typo(self):
+        watch_list = ('{"DEFINTIONS": {}}')
+        OutputCapture().assert_outputs(self, self._watch_list_parser.parse, args=[watch_list],
+                                       expected_logs='Unknown section "DEFINTIONS" in watch list.'
+                                       + '\n\nPerhaps it should be DEFINITIONS.\n')
+
+    def test_bad_definition(self):
+        watch_list = (
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1|A": {'
+            '            "filename": r".*\\MyFileName\\.cpp",'
+            '        },'
+            '     },'
+            '}')
+
+        OutputCapture().assert_outputs(self, self._watch_list_parser.parse, args=[watch_list],
+                                       expected_logs='Invalid character "|" in definition "WatchList1|A".\n')
+
+    def test_bad_filename_regex(self):
+        watch_list = (
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "filename": r"*",'
+            '            "more": r"RefCounted",'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList1": ["levin@chromium.org"],'
+            '     },'
+            '}')
+
+        OutputCapture().assert_outputs(self, self._watch_list_parser.parse, args=[watch_list],
+                                       expected_logs='The regex "*" is invalid due to "nothing to repeat".\n')
+
+    def test_bad_more_regex(self):
+        watch_list = (
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "filename": r"aFileName\\.cpp",'
+            '            "more": r"*",'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList1": ["levin@chromium.org"],'
+            '     },'
+            '}')
+
+        OutputCapture().assert_outputs(self, self._watch_list_parser.parse, args=[watch_list],
+                                       expected_logs='The regex "*" is invalid due to "nothing to repeat".\n')
+
+    def test_bad_match_type(self):
+        watch_list = (
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "nothing_matches_this": r".*\\MyFileName\\.cpp",'
+            '            "filename": r".*\\MyFileName\\.cpp",'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList1": ["levin@chromium.org"],'
+            '     },'
+            '}')
+
+        OutputCapture().assert_outputs(self, self._watch_list_parser.parse, args=[watch_list],
+                                       expected_logs='Unknown pattern type "nothing_matches_this" in definition "WatchList1".\n')
+
+    def test_match_type_typo(self):
+        watch_list = (
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "iflename": r".*\\MyFileName\\.cpp",'
+            '            "more": r"RefCounted",'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList1": ["levin@chromium.org"],'
+            '     },'
+            '}')
+
+        OutputCapture().assert_outputs(self, self._watch_list_parser.parse, args=[watch_list],
+                                       expected_logs='Unknown pattern type "iflename" in definition "WatchList1".'
+                                       + '\n\nPerhaps it should be filename.\n')
+
+    def test_empty_definition(self):
+        watch_list = (
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList1": ["levin@chromium.org"],'
+            '     },'
+            '}')
+
+        OutputCapture().assert_outputs(self, self._watch_list_parser.parse, args=[watch_list],
+                                       expected_logs='The definition "WatchList1" has no patterns, so it should be deleted.\n')
+
+    def test_empty_cc_rule(self):
+        watch_list = (
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "filename": r".*\\MyFileName\\.cpp",'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList1": [],'
+            '     },'
+            '}')
+
+        OutputCapture().assert_outputs(self, self._watch_list_parser.parse, args=[watch_list],
+                                       expected_logs='A rule for definition "WatchList1" is empty, so it should be deleted.\n'
+                                       + 'The following definitions are not used and should be removed: WatchList1\n')
+
+    def test_cc_rule_with_invalid_email(self):
+        watch_list = (
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "filename": r".*\\MyFileName\\.cpp",'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList1": ["levin+bad+email@chromium.org"],'
+            '     },'
+            '}')
+
+        OutputCapture().assert_outputs(self, self._watch_list_parser.parse, args=[watch_list],
+                                       expected_logs='The email alias levin+bad+email@chromium.org which is'
+                                       + ' in the watchlist is not listed as a contributor in committers.py\n')
+
+    def test_empty_message_rule(self):
+        watch_list = (
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "filename": r".*\\MyFileName\\.cpp",'
+            '        },'
+            '     },'
+            '    "MESSAGE_RULES": {'
+            '        "WatchList1": ['
+            '        ],'
+            '     },'
+            '}')
+
+        OutputCapture().assert_outputs(self, self._watch_list_parser.parse, args=[watch_list],
+                                       expected_logs='A rule for definition "WatchList1" is empty, so it should be deleted.\n'
+                                       + 'The following definitions are not used and should be removed: WatchList1\n')
+
+    def test_unused_defintion(self):
+        watch_list = (
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "filename": r".*\\MyFileName\\.cpp",'
+            '        },'
+            '     },'
+            '}')
+
+        OutputCapture().assert_outputs(self, self._watch_list_parser.parse, args=[watch_list],
+                                       expected_logs='The following definitions are not used and should be removed: WatchList1\n')
+
+    def test_cc_rule_with_undefined_defintion(self):
+        watch_list = (
+            '{'
+            '    "CC_RULES": {'
+            '        "WatchList1": ["levin@chromium.org"]'
+            '     },'
+            '}')
+
+        OutputCapture().assert_outputs(self, self._watch_list_parser.parse, args=[watch_list],
+                                       expected_logs='In section "CC_RULES", the following definitions are not used and should be removed: WatchList1\n')
+
+    def test_message_rule_with_undefined_defintion(self):
+        watch_list = (
+            '{'
+            '    "MESSAGE_RULES": {'
+            '        "WatchList1": ["The message."]'
+            '     },'
+            '}')
+
+        OutputCapture().assert_outputs(self, self._watch_list_parser.parse, args=[watch_list],
+                                       expected_logs='In section "MESSAGE_RULES", the following definitions are not used and should be removed: WatchList1\n')
+
+    def test_cc_rule_with_undefined_defintion_with_suggestion(self):
+        watch_list = (
+            '{'
+            '    "DEFINITIONS": {'
+            '        "WatchList1": {'
+            '            "filename": r".*\\MyFileName\\.cpp",'
+            '        },'
+            '     },'
+            '    "CC_RULES": {'
+            '        "WatchList": ["levin@chromium.org"]'
+            '     },'
+            '    "MESSAGE_RULES": {'
+            '        "WatchList1": ["levin@chromium.org"]'
+            '     },'
+            '}')
+
+        OutputCapture().assert_outputs(self, self._watch_list_parser.parse, args=[watch_list],
+                                       expected_logs='In section "CC_RULES", the following definitions are not used and should be removed: WatchList'
+                                       + '\n\nPerhaps it should be WatchList1.\n')
diff --git a/Tools/Scripts/webkitpy/common/watchlist/watchlistrule.py b/Tools/Scripts/webkitpy/common/watchlist/watchlistrule.py
new file mode 100644
index 0000000..6987508
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/watchlist/watchlistrule.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2011 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 WatchListRule:
+    '''A rule with instructions to do when the rule is satisified.'''
+    def __init__(self, complex_definition, instructions):
+        self.definitions_to_match = complex_definition.split('|')
+        self._instructions = instructions
+
+    def match(self, matching_definitions):
+        for test_definition in self.definitions_to_match:
+            if test_definition in matching_definitions:
+                return True
+        return False
+
+    def instructions(self):
+        return self._instructions
+
+    def remove_instruction(self, instruction):
+        self._instructions.remove(instruction)
diff --git a/Tools/Scripts/webkitpy/common/watchlist/watchlistrule_unittest.py b/Tools/Scripts/webkitpy/common/watchlist/watchlistrule_unittest.py
new file mode 100644
index 0000000..92aaf34
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/watchlist/watchlistrule_unittest.py
@@ -0,0 +1,62 @@
+# Copyright (C) 2011 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.watchlist.watchlistrule import WatchListRule
+
+
+class WatchListRuleTest(unittest.TestCase):
+    def test_instruction_list(self):
+        instructions = ['a', 'b']
+        rule = WatchListRule('definition1', instructions[:])
+        self.assertEqual(instructions, rule.instructions())
+
+    def test_remove_instruction(self):
+        instructions = ['a', 'b']
+        rule = WatchListRule('definition1', instructions[:])
+        rule.remove_instruction('b')
+        self.assertEqual(['a'], rule.instructions())
+
+    def test_simple_definition(self):
+        definition_name = 'definition1'
+        rule = WatchListRule(definition_name, [])
+        self.assertTrue(rule.match([definition_name]))
+        self.assertFalse(rule.match([definition_name + '1']))
+
+    def test_complex_definition(self):
+        definition_name1 = 'definition1'
+        definition_name2 = 'definition2'
+        definition_name3 = 'definition3'
+        rule = WatchListRule(definition_name1 + '|' + definition_name2 + '|' + definition_name3, [])
+        self.assertTrue(rule.match([definition_name1]))
+        self.assertTrue(rule.match([definition_name2]))
+        self.assertTrue(rule.match([definition_name3]))
+        self.assertFalse(rule.match([definition_name1 + '1']))
+        self.assertFalse(rule.match([definition_name2 + '1']))
+        self.assertFalse(rule.match([definition_name3 + '1']))
diff --git a/Tools/Scripts/webkitpy/common/webkitunittest.py b/Tools/Scripts/webkitpy/common/webkitunittest.py
new file mode 100644
index 0000000..7b650a1
--- /dev/null
+++ b/Tools/Scripts/webkitpy/common/webkitunittest.py
@@ -0,0 +1,49 @@
+# Copyright (C) 2011 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.
+
+'''Basic unit test functionality.'''
+
+import re
+import unittest
+
+
+class TestCase(unittest.TestCase):
+    def setUp(self):
+        # For versions of Python before 2.7.
+        if not 'assertRaisesRegexp' in dir(self):
+            self.assertRaisesRegexp = self._assertRaisesRegexp
+
+    def _assertRaisesRegexp(self, expected_exception, regex_message, callable, *args):
+        try:
+            callable(*args)
+            self.assertTrue(False, 'No assert raised.')
+        except Exception, exception:
+            self.assertTrue(issubclass(exception.__class__, expected_exception),
+                            'Exception type was unexpected.')
+            self.assertTrue(re.match(regex_message, exception.__str__()),
+                            'Expected regex "%s"\nGot "%s"' % (regex_message, exception.__str__()))
diff --git a/Tools/Scripts/webkitpy/layout_tests/__init__.py b/Tools/Scripts/webkitpy/layout_tests/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/layout_tests/controllers/__init__.py b/Tools/Scripts/webkitpy/layout_tests/controllers/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/controllers/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/layout_tests/controllers/layout_test_finder.py b/Tools/Scripts/webkitpy/layout_tests/controllers/layout_test_finder.py
new file mode 100644
index 0000000..6447c8f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/controllers/layout_test_finder.py
@@ -0,0 +1,176 @@
+# Copyright (C) 2012 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 errno
+import logging
+import re
+
+from webkitpy.layout_tests.models import test_expectations
+
+
+_log = logging.getLogger(__name__)
+
+
+class LayoutTestFinder(object):
+    def __init__(self, port, options):
+        self._port = port
+        self._options = options
+        self._filesystem = self._port.host.filesystem
+        self.LAYOUT_TESTS_DIRECTORY = 'LayoutTests'
+
+    def find_tests(self, options, args):
+        paths = self._strip_test_dir_prefixes(args)
+        if options.test_list:
+            paths += self._strip_test_dir_prefixes(self._read_test_names_from_file(options.test_list, self._port.TEST_PATH_SEPARATOR))
+        paths = set(paths)
+        test_files = self._port.tests(paths)
+        return (paths, test_files)
+
+    def _strip_test_dir_prefixes(self, paths):
+        return [self._strip_test_dir_prefix(path) for path in paths if path]
+
+    def _strip_test_dir_prefix(self, path):
+        # Handle both "LayoutTests/foo/bar.html" and "LayoutTests\foo\bar.html" if
+        # the filesystem uses '\\' as a directory separator.
+        if path.startswith(self.LAYOUT_TESTS_DIRECTORY + self._port.TEST_PATH_SEPARATOR):
+            return path[len(self.LAYOUT_TESTS_DIRECTORY + self._port.TEST_PATH_SEPARATOR):]
+        if path.startswith(self.LAYOUT_TESTS_DIRECTORY + self._filesystem.sep):
+            return path[len(self.LAYOUT_TESTS_DIRECTORY + self._filesystem.sep):]
+        return path
+
+    def _read_test_names_from_file(self, filenames, test_path_separator):
+        fs = self._filesystem
+        tests = []
+        for filename in filenames:
+            try:
+                if test_path_separator != fs.sep:
+                    filename = filename.replace(test_path_separator, fs.sep)
+                file_contents = fs.read_text_file(filename).split('\n')
+                for line in file_contents:
+                    line = self._strip_comments(line)
+                    if line:
+                        tests.append(line)
+            except IOError, e:
+                if e.errno == errno.ENOENT:
+                    _log.critical('')
+                    _log.critical('--test-list file "%s" not found' % file)
+                raise
+        return tests
+
+    @staticmethod
+    def _strip_comments(line):
+        commentIndex = line.find('//')
+        if commentIndex is -1:
+            commentIndex = len(line)
+
+        line = re.sub(r'\s+', ' ', line[:commentIndex].strip())
+        if line == '':
+            return None
+        else:
+            return line
+
+    def skip_tests(self, paths, all_tests_list, expectations, http_tests):
+        all_tests = set(all_tests_list)
+
+        tests_to_skip = expectations.get_tests_with_result_type(test_expectations.SKIP)
+        if self._options.skip_failing_tests:
+            tests_to_skip.update(expectations.get_tests_with_result_type(test_expectations.FAIL))
+            tests_to_skip.update(expectations.get_tests_with_result_type(test_expectations.FLAKY))
+
+        if self._options.skipped == 'only':
+            tests_to_skip = all_tests - tests_to_skip
+        elif self._options.skipped == 'ignore':
+            tests_to_skip = set()
+        elif self._options.skipped != 'always':
+            # make sure we're explicitly running any tests passed on the command line; equivalent to 'default'.
+            tests_to_skip -= paths
+
+        # unless of course we don't want to run the HTTP tests :)
+        if not self._options.http:
+            tests_to_skip.update(set(http_tests))
+
+        return tests_to_skip
+
+    def split_into_chunks(self, test_names):
+        """split into a list to run and a set to skip, based on --run-chunk and --run-part."""
+        if not self._options.run_chunk and not self._options.run_part:
+            return test_names, set()
+
+        # If the user specifies they just want to run a subset of the tests,
+        # just grab a subset of the non-skipped tests.
+        chunk_value = self._options.run_chunk or self._options.run_part
+        try:
+            (chunk_num, chunk_len) = chunk_value.split(":")
+            chunk_num = int(chunk_num)
+            assert(chunk_num >= 0)
+            test_size = int(chunk_len)
+            assert(test_size > 0)
+        except AssertionError:
+            _log.critical("invalid chunk '%s'" % chunk_value)
+            return (None, None)
+
+        # Get the number of tests
+        num_tests = len(test_names)
+
+        # Get the start offset of the slice.
+        if self._options.run_chunk:
+            chunk_len = test_size
+            # In this case chunk_num can be really large. We need
+            # to make the slave fit in the current number of tests.
+            slice_start = (chunk_num * chunk_len) % num_tests
+        else:
+            # Validate the data.
+            assert(test_size <= num_tests)
+            assert(chunk_num <= test_size)
+
+            # To count the chunk_len, and make sure we don't skip
+            # some tests, we round to the next value that fits exactly
+            # all the parts.
+            rounded_tests = num_tests
+            if rounded_tests % test_size != 0:
+                rounded_tests = (num_tests + test_size - (num_tests % test_size))
+
+            chunk_len = rounded_tests / test_size
+            slice_start = chunk_len * (chunk_num - 1)
+            # It does not mind if we go over test_size.
+
+        # Get the end offset of the slice.
+        slice_end = min(num_tests, slice_start + chunk_len)
+
+        tests_to_run = test_names[slice_start:slice_end]
+
+        _log.debug('chunk slice [%d:%d] of %d is %d tests' % (slice_start, slice_end, num_tests, (slice_end - slice_start)))
+
+        # If we reached the end and we don't have enough tests, we run some
+        # from the beginning.
+        if slice_end - slice_start < chunk_len:
+            extra = chunk_len - (slice_end - slice_start)
+            _log.debug('   last chunk is partial, appending [0:%d]' % extra)
+            tests_to_run.extend(test_names[0:extra])
+
+        return (tests_to_run, set(test_names) - set(tests_to_run))
diff --git a/Tools/Scripts/webkitpy/layout_tests/controllers/layout_test_runner.py b/Tools/Scripts/webkitpy/layout_tests/controllers/layout_test_runner.py
new file mode 100644
index 0000000..17cbe31
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/controllers/layout_test_runner.py
@@ -0,0 +1,631 @@
+# Copyright (C) 2011 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 logging
+import math
+import re
+import threading
+import time
+
+from webkitpy.common import message_pool
+from webkitpy.layout_tests.controllers import single_test_runner
+from webkitpy.layout_tests.models import test_expectations
+from webkitpy.layout_tests.models import test_failures
+from webkitpy.layout_tests.models import test_results
+from webkitpy.tool import grammar
+
+
+_log = logging.getLogger(__name__)
+
+
+TestExpectations = test_expectations.TestExpectations
+
+# Export this so callers don't need to know about message pools.
+WorkerException = message_pool.WorkerException
+
+
+class TestRunInterruptedException(Exception):
+    """Raised when a test run should be stopped immediately."""
+    def __init__(self, reason):
+        Exception.__init__(self)
+        self.reason = reason
+        self.msg = reason
+
+    def __reduce__(self):
+        return self.__class__, (self.reason,)
+
+
+class LayoutTestRunner(object):
+    def __init__(self, options, port, printer, results_directory, expectations, test_is_slow_fn):
+        self._options = options
+        self._port = port
+        self._printer = printer
+        self._results_directory = results_directory
+        self._expectations = None
+        self._test_is_slow = test_is_slow_fn
+        self._sharder = Sharder(self._port.split_test, self._port.TEST_PATH_SEPARATOR, self._options.max_locked_shards)
+
+        self._current_result_summary = None
+        self._needs_http = None
+        self._needs_websockets = None
+        self._retrying = False
+        self._test_files_list = []
+        self._all_results = []
+        self._group_stats = {}
+        self._worker_stats = {}
+        self._filesystem = self._port.host.filesystem
+
+    def test_key(self, test_name):
+        return self._sharder.test_key(test_name)
+
+    def run_tests(self, test_inputs, expectations, result_summary, num_workers, needs_http, needs_websockets, retrying):
+        """Returns a tuple of (interrupted, keyboard_interrupted, thread_timings, test_timings, individual_test_timings):
+            interrupted is whether the run was interrupted
+            keyboard_interrupted is whether the interruption was because someone typed Ctrl^C
+            thread_timings is a list of dicts with the total runtime
+                of each thread with 'name', 'num_tests', 'total_time' properties
+            test_timings is a list of timings for each sharded subdirectory
+                of the form [time, directory_name, num_tests]
+            individual_test_timings is a list of run times for each test
+                in the form {filename:filename, test_run_time:test_run_time}
+            result_summary: summary object to populate with the results
+        """
+        self._current_result_summary = result_summary
+        self._expectations = expectations
+        self._needs_http = needs_http
+        self._needs_websockets = needs_websockets
+        self._retrying = retrying
+        self._test_files_list = [test_input.test_name for test_input in test_inputs]
+        self._printer.num_tests = len(self._test_files_list)
+        self._printer.num_completed = 0
+
+        self._all_results = []
+        self._group_stats = {}
+        self._worker_stats = {}
+        self._has_http_lock = False
+        self._remaining_locked_shards = []
+
+        keyboard_interrupted = False
+        interrupted = False
+
+        self._printer.write_update('Sharding tests ...')
+        locked_shards, unlocked_shards = self._sharder.shard_tests(test_inputs, int(self._options.child_processes), self._options.fully_parallel)
+
+        # FIXME: We don't have a good way to coordinate the workers so that
+        # they don't try to run the shards that need a lock if we don't actually
+        # have the lock. The easiest solution at the moment is to grab the
+        # lock at the beginning of the run, and then run all of the locked
+        # shards first. This minimizes the time spent holding the lock, but
+        # means that we won't be running tests while we're waiting for the lock.
+        # If this becomes a problem in practice we'll need to change this.
+
+        all_shards = locked_shards + unlocked_shards
+        self._remaining_locked_shards = locked_shards
+        if self._port.requires_http_server() or (locked_shards and self._options.http):
+            self.start_servers_with_lock(2 * min(num_workers, len(locked_shards)))
+
+        num_workers = min(num_workers, len(all_shards))
+        self._printer.print_workers_and_shards(num_workers, len(all_shards), len(locked_shards))
+
+        if self._options.dry_run:
+            return (keyboard_interrupted, interrupted, self._worker_stats.values(), self._group_stats, self._all_results)
+
+        self._printer.write_update('Starting %s ...' % grammar.pluralize('worker', num_workers))
+
+        try:
+            with message_pool.get(self, self._worker_factory, num_workers, self._port.worker_startup_delay_secs(), self._port.host) as pool:
+                pool.run(('test_list', shard.name, shard.test_inputs) for shard in all_shards)
+        except KeyboardInterrupt:
+            self._printer.flush()
+            self._printer.writeln('Interrupted, exiting ...')
+            keyboard_interrupted = True
+        except TestRunInterruptedException, e:
+            _log.warning(e.reason)
+            interrupted = True
+        except Exception, e:
+            _log.debug('%s("%s") raised, exiting' % (e.__class__.__name__, str(e)))
+            raise
+        finally:
+            self.stop_servers_with_lock()
+
+        # FIXME: should this be a class instead of a tuple?
+        return (interrupted, keyboard_interrupted, self._worker_stats.values(), self._group_stats, self._all_results)
+
+    def _worker_factory(self, worker_connection):
+        results_directory = self._results_directory
+        if self._retrying:
+            self._filesystem.maybe_make_directory(self._filesystem.join(self._results_directory, 'retries'))
+            results_directory = self._filesystem.join(self._results_directory, 'retries')
+        return Worker(worker_connection, results_directory, self._options)
+
+    def _mark_interrupted_tests_as_skipped(self, result_summary):
+        for test_name in self._test_files_list:
+            if test_name not in result_summary.results:
+                result = test_results.TestResult(test_name, [test_failures.FailureEarlyExit()])
+                # FIXME: We probably need to loop here if there are multiple iterations.
+                # FIXME: Also, these results are really neither expected nor unexpected. We probably
+                # need a third type of result.
+                result_summary.add(result, expected=False, test_is_slow=self._test_is_slow(test_name))
+
+    def _interrupt_if_at_failure_limits(self, result_summary):
+        # Note: The messages in this method are constructed to match old-run-webkit-tests
+        # so that existing buildbot grep rules work.
+        def interrupt_if_at_failure_limit(limit, failure_count, result_summary, message):
+            if limit and failure_count >= limit:
+                message += " %d tests run." % (result_summary.expected + result_summary.unexpected)
+                self._mark_interrupted_tests_as_skipped(result_summary)
+                raise TestRunInterruptedException(message)
+
+        interrupt_if_at_failure_limit(
+            self._options.exit_after_n_failures,
+            result_summary.unexpected_failures,
+            result_summary,
+            "Exiting early after %d failures." % result_summary.unexpected_failures)
+        interrupt_if_at_failure_limit(
+            self._options.exit_after_n_crashes_or_timeouts,
+            result_summary.unexpected_crashes + result_summary.unexpected_timeouts,
+            result_summary,
+            # This differs from ORWT because it does not include WebProcess crashes.
+            "Exiting early after %d crashes and %d timeouts." % (result_summary.unexpected_crashes, result_summary.unexpected_timeouts))
+
+    def _update_summary_with_result(self, result_summary, result):
+        if result.type == test_expectations.SKIP:
+            exp_str = got_str = 'SKIP'
+            expected = True
+        else:
+            expected = self._expectations.matches_an_expected_result(result.test_name, result.type, self._options.pixel_tests or result.reftest_type)
+            exp_str = self._expectations.get_expectations_string(result.test_name)
+            got_str = self._expectations.expectation_to_string(result.type)
+
+        result_summary.add(result, expected, self._test_is_slow(result.test_name))
+
+        self._printer.print_finished_test(result, expected, exp_str, got_str)
+
+        self._interrupt_if_at_failure_limits(result_summary)
+
+    def start_servers_with_lock(self, number_of_servers):
+        self._printer.write_update('Acquiring http lock ...')
+        self._port.acquire_http_lock()
+        if self._needs_http:
+            self._printer.write_update('Starting HTTP server ...')
+            self._port.start_http_server(number_of_servers=number_of_servers)
+        if self._needs_websockets:
+            self._printer.write_update('Starting WebSocket server ...')
+            self._port.start_websocket_server()
+        self._has_http_lock = True
+
+    def stop_servers_with_lock(self):
+        if self._has_http_lock:
+            if self._needs_http:
+                self._printer.write_update('Stopping HTTP server ...')
+                self._port.stop_http_server()
+            if self._needs_websockets:
+                self._printer.write_update('Stopping WebSocket server ...')
+                self._port.stop_websocket_server()
+            self._printer.write_update('Releasing server lock ...')
+            self._port.release_http_lock()
+            self._has_http_lock = False
+
+    def handle(self, name, source, *args):
+        method = getattr(self, '_handle_' + name)
+        if method:
+            return method(source, *args)
+        raise AssertionError('unknown message %s received from %s, args=%s' % (name, source, repr(args)))
+
+    def _handle_started_test(self, worker_name, test_input, test_timeout_sec):
+        self._printer.print_started_test(test_input.test_name)
+
+    def _handle_finished_test_list(self, worker_name, list_name, num_tests, elapsed_time):
+        self._group_stats[list_name] = (num_tests, elapsed_time)
+
+        def find(name, test_lists):
+            for i in range(len(test_lists)):
+                if test_lists[i].name == name:
+                    return i
+            return -1
+
+        index = find(list_name, self._remaining_locked_shards)
+        if index >= 0:
+            self._remaining_locked_shards.pop(index)
+            if not self._remaining_locked_shards and not self._port.requires_http_server():
+                self.stop_servers_with_lock()
+
+    def _handle_finished_test(self, worker_name, result, elapsed_time, log_messages=[]):
+        self._worker_stats.setdefault(worker_name, {'name': worker_name, 'num_tests': 0, 'total_time': 0})
+        self._worker_stats[worker_name]['total_time'] += elapsed_time
+        self._worker_stats[worker_name]['num_tests'] += 1
+        self._all_results.append(result)
+        self._update_summary_with_result(self._current_result_summary, result)
+
+
+class Worker(object):
+    def __init__(self, caller, results_directory, options):
+        self._caller = caller
+        self._worker_number = caller.worker_number
+        self._name = caller.name
+        self._results_directory = results_directory
+        self._options = options
+
+        # The remaining fields are initialized in start()
+        self._host = None
+        self._port = None
+        self._batch_size = None
+        self._batch_count = None
+        self._filesystem = None
+        self._driver = None
+        self._tests_run_file = None
+        self._tests_run_filename = None
+
+    def __del__(self):
+        self.stop()
+
+    def start(self):
+        """This method is called when the object is starting to be used and it is safe
+        for the object to create state that does not need to be pickled (usually this means
+        it is called in a child process)."""
+        self._host = self._caller.host
+        self._filesystem = self._host.filesystem
+        self._port = self._host.port_factory.get(self._options.platform, self._options)
+
+        self._batch_count = 0
+        self._batch_size = self._options.batch_size or 0
+        tests_run_filename = self._filesystem.join(self._results_directory, "tests_run%d.txt" % self._worker_number)
+        self._tests_run_file = self._filesystem.open_text_file_for_writing(tests_run_filename)
+
+    def handle(self, name, source, test_list_name, test_inputs):
+        assert name == 'test_list'
+        start_time = time.time()
+        for test_input in test_inputs:
+            self._run_test(test_input)
+        elapsed_time = time.time() - start_time
+        self._caller.post('finished_test_list', test_list_name, len(test_inputs), elapsed_time)
+
+    def _update_test_input(self, test_input):
+        if test_input.reference_files is None:
+            # Lazy initialization.
+            test_input.reference_files = self._port.reference_files(test_input.test_name)
+        if test_input.reference_files:
+            test_input.should_run_pixel_test = True
+        else:
+            test_input.should_run_pixel_test = self._port.should_run_as_pixel_test(test_input)
+
+    def _run_test(self, test_input):
+        self._batch_count += 1
+
+        stop_when_done = False
+        if self._batch_size > 0 and self._batch_count >= self._batch_size:
+            self._batch_count = 0
+            stop_when_done = True
+
+        self._update_test_input(test_input)
+        test_timeout_sec = self._timeout(test_input)
+        start = time.time()
+        self._caller.post('started_test', test_input, test_timeout_sec)
+
+        result = self._run_test_with_timeout(test_input, test_timeout_sec, stop_when_done)
+
+        elapsed_time = time.time() - start
+        self._caller.post('finished_test', result, elapsed_time)
+
+        self._clean_up_after_test(test_input, result)
+
+    def stop(self):
+        _log.debug("%s cleaning up" % self._name)
+        self._kill_driver()
+        if self._tests_run_file:
+            self._tests_run_file.close()
+            self._tests_run_file = None
+
+    def _timeout(self, test_input):
+        """Compute the appropriate timeout value for a test."""
+        # The DumpRenderTree watchdog uses 2.5x the timeout; we want to be
+        # larger than that. We also add a little more padding if we're
+        # running tests in a separate thread.
+        #
+        # Note that we need to convert the test timeout from a
+        # string value in milliseconds to a float for Python.
+        driver_timeout_sec = 3.0 * float(test_input.timeout) / 1000.0
+        if not self._options.run_singly:
+            return driver_timeout_sec
+
+        thread_padding_sec = 1.0
+        thread_timeout_sec = driver_timeout_sec + thread_padding_sec
+        return thread_timeout_sec
+
+    def _kill_driver(self):
+        # Be careful about how and when we kill the driver; if driver.stop()
+        # raises an exception, this routine may get re-entered via __del__.
+        driver = self._driver
+        self._driver = None
+        if driver:
+            _log.debug("%s killing driver" % self._name)
+            driver.stop()
+
+    def _run_test_with_timeout(self, test_input, timeout, stop_when_done):
+        if self._options.run_singly:
+            return self._run_test_in_another_thread(test_input, timeout, stop_when_done)
+        return self._run_test_in_this_thread(test_input, stop_when_done)
+
+    def _clean_up_after_test(self, test_input, result):
+        test_name = test_input.test_name
+        self._tests_run_file.write(test_name + "\n")
+
+        if result.failures:
+            # Check and kill DumpRenderTree if we need to.
+            if any([f.driver_needs_restart() for f in result.failures]):
+                self._kill_driver()
+                # Reset the batch count since the shell just bounced.
+                self._batch_count = 0
+
+            # Print the error message(s).
+            _log.debug("%s %s failed:" % (self._name, test_name))
+            for f in result.failures:
+                _log.debug("%s  %s" % (self._name, f.message()))
+        elif result.type == test_expectations.SKIP:
+            _log.debug("%s %s skipped" % (self._name, test_name))
+        else:
+            _log.debug("%s %s passed" % (self._name, test_name))
+
+    def _run_test_in_another_thread(self, test_input, thread_timeout_sec, stop_when_done):
+        """Run a test in a separate thread, enforcing a hard time limit.
+
+        Since we can only detect the termination of a thread, not any internal
+        state or progress, we can only run per-test timeouts when running test
+        files singly.
+
+        Args:
+          test_input: Object containing the test filename and timeout
+          thread_timeout_sec: time to wait before killing the driver process.
+        Returns:
+          A TestResult
+        """
+        worker = self
+
+        driver = self._port.create_driver(self._worker_number)
+
+        class SingleTestThread(threading.Thread):
+            def __init__(self):
+                threading.Thread.__init__(self)
+                self.result = None
+
+            def run(self):
+                self.result = worker._run_single_test(driver, test_input, stop_when_done)
+
+        thread = SingleTestThread()
+        thread.start()
+        thread.join(thread_timeout_sec)
+        result = thread.result
+        failures = []
+        if thread.isAlive():
+            # If join() returned with the thread still running, the
+            # DumpRenderTree is completely hung and there's nothing
+            # more we can do with it.  We have to kill all the
+            # DumpRenderTrees to free it up. If we're running more than
+            # one DumpRenderTree thread, we'll end up killing the other
+            # DumpRenderTrees too, introducing spurious crashes. We accept
+            # that tradeoff in order to avoid losing the rest of this
+            # thread's results.
+            _log.error('Test thread hung: killing all DumpRenderTrees')
+            failures = [test_failures.FailureTimeout()]
+
+        driver.stop()
+
+        if not result:
+            result = test_results.TestResult(test_input.test_name, failures=failures, test_run_time=0)
+        return result
+
+    def _run_test_in_this_thread(self, test_input, stop_when_done):
+        """Run a single test file using a shared DumpRenderTree process.
+
+        Args:
+          test_input: Object containing the test filename, uri and timeout
+
+        Returns: a TestResult object.
+        """
+        if self._driver and self._driver.has_crashed():
+            self._kill_driver()
+        if not self._driver:
+            self._driver = self._port.create_driver(self._worker_number)
+        return self._run_single_test(self._driver, test_input, stop_when_done)
+
+    def _run_single_test(self, driver, test_input, stop_when_done):
+        return single_test_runner.run_single_test(self._port, self._options,
+            test_input, driver, self._name, stop_when_done)
+
+
+class TestShard(object):
+    """A test shard is a named list of TestInputs."""
+
+    def __init__(self, name, test_inputs):
+        self.name = name
+        self.test_inputs = test_inputs
+        self.requires_lock = test_inputs[0].requires_lock
+
+    def __repr__(self):
+        return "TestShard(name='%s', test_inputs=%s, requires_lock=%s'" % (self.name, self.test_inputs, self.requires_lock)
+
+    def __eq__(self, other):
+        return self.name == other.name and self.test_inputs == other.test_inputs
+
+
+class Sharder(object):
+    def __init__(self, test_split_fn, test_path_separator, max_locked_shards):
+        self._split = test_split_fn
+        self._sep = test_path_separator
+        self._max_locked_shards = max_locked_shards
+
+    def shard_tests(self, test_inputs, num_workers, fully_parallel):
+        """Groups tests into batches.
+        This helps ensure that tests that depend on each other (aka bad tests!)
+        continue to run together as most cross-tests dependencies tend to
+        occur within the same directory.
+        Return:
+            Two list of TestShards. The first contains tests that must only be
+            run under the server lock, the second can be run whenever.
+        """
+
+        # FIXME: Move all of the sharding logic out of manager into its
+        # own class or module. Consider grouping it with the chunking logic
+        # in prepare_lists as well.
+        if num_workers == 1:
+            return self._shard_in_two(test_inputs)
+        elif fully_parallel:
+            return self._shard_every_file(test_inputs)
+        return self._shard_by_directory(test_inputs, num_workers)
+
+    def _shard_in_two(self, test_inputs):
+        """Returns two lists of shards, one with all the tests requiring a lock and one with the rest.
+
+        This is used when there's only one worker, to minimize the per-shard overhead."""
+        locked_inputs = []
+        unlocked_inputs = []
+        for test_input in test_inputs:
+            if test_input.requires_lock:
+                locked_inputs.append(test_input)
+            else:
+                unlocked_inputs.append(test_input)
+
+        locked_shards = []
+        unlocked_shards = []
+        if locked_inputs:
+            locked_shards = [TestShard('locked_tests', locked_inputs)]
+        if unlocked_inputs:
+            unlocked_shards = [TestShard('unlocked_tests', unlocked_inputs)]
+
+        return locked_shards, unlocked_shards
+
+    def _shard_every_file(self, test_inputs):
+        """Returns two lists of shards, each shard containing a single test file.
+
+        This mode gets maximal parallelism at the cost of much higher flakiness."""
+        locked_shards = []
+        unlocked_shards = []
+        for test_input in test_inputs:
+            # Note that we use a '.' for the shard name; the name doesn't really
+            # matter, and the only other meaningful value would be the filename,
+            # which would be really redundant.
+            if test_input.requires_lock:
+                locked_shards.append(TestShard('.', [test_input]))
+            else:
+                unlocked_shards.append(TestShard('.', [test_input]))
+
+        return locked_shards, unlocked_shards
+
+    def _shard_by_directory(self, test_inputs, num_workers):
+        """Returns two lists of shards, each shard containing all the files in a directory.
+
+        This is the default mode, and gets as much parallelism as we can while
+        minimizing flakiness caused by inter-test dependencies."""
+        locked_shards = []
+        unlocked_shards = []
+        tests_by_dir = {}
+        # FIXME: Given that the tests are already sorted by directory,
+        # we can probably rewrite this to be clearer and faster.
+        for test_input in test_inputs:
+            directory = self._split(test_input.test_name)[0]
+            tests_by_dir.setdefault(directory, [])
+            tests_by_dir[directory].append(test_input)
+
+        for directory, test_inputs in tests_by_dir.iteritems():
+            shard = TestShard(directory, test_inputs)
+            if test_inputs[0].requires_lock:
+                locked_shards.append(shard)
+            else:
+                unlocked_shards.append(shard)
+
+        # Sort the shards by directory name.
+        locked_shards.sort(key=lambda shard: shard.name)
+        unlocked_shards.sort(key=lambda shard: shard.name)
+
+        # Put a ceiling on the number of locked shards, so that we
+        # don't hammer the servers too badly.
+
+        # FIXME: For now, limit to one shard or set it
+        # with the --max-locked-shards. After testing to make sure we
+        # can handle multiple shards, we should probably do something like
+        # limit this to no more than a quarter of all workers, e.g.:
+        # return max(math.ceil(num_workers / 4.0), 1)
+        return (self._resize_shards(locked_shards, self._max_locked_shards, 'locked_shard'),
+                unlocked_shards)
+
+    def _resize_shards(self, old_shards, max_new_shards, shard_name_prefix):
+        """Takes a list of shards and redistributes the tests into no more
+        than |max_new_shards| new shards."""
+
+        # This implementation assumes that each input shard only contains tests from a
+        # single directory, and that tests in each shard must remain together; as a
+        # result, a given input shard is never split between output shards.
+        #
+        # Each output shard contains the tests from one or more input shards and
+        # hence may contain tests from multiple directories.
+
+        def divide_and_round_up(numerator, divisor):
+            return int(math.ceil(float(numerator) / divisor))
+
+        def extract_and_flatten(shards):
+            test_inputs = []
+            for shard in shards:
+                test_inputs.extend(shard.test_inputs)
+            return test_inputs
+
+        def split_at(seq, index):
+            return (seq[:index], seq[index:])
+
+        num_old_per_new = divide_and_round_up(len(old_shards), max_new_shards)
+        new_shards = []
+        remaining_shards = old_shards
+        while remaining_shards:
+            some_shards, remaining_shards = split_at(remaining_shards, num_old_per_new)
+            new_shards.append(TestShard('%s_%d' % (shard_name_prefix, len(new_shards) + 1), extract_and_flatten(some_shards)))
+        return new_shards
+
+    def test_key(self, test_name):
+        """Turns a test name into a list with two sublists, the natural key of the
+        dirname, and the natural key of the basename.
+
+        This can be used when sorting paths so that files in a directory.
+        directory are kept together rather than being mixed in with files in
+        subdirectories."""
+        dirname, basename = self._split(test_name)
+        return (self.natural_sort_key(dirname + self._sep), self.natural_sort_key(basename))
+
+    @staticmethod
+    def natural_sort_key(string_to_split):
+        """ Turns a string into a list of string and number chunks, i.e. "z23a" -> ["z", 23, "a"]
+
+        This can be used to implement "natural sort" order. See:
+        http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html
+        http://nedbatchelder.com/blog/200712.html#e20071211T054956
+        """
+        def tryint(val):
+            try:
+                return int(val)
+            except ValueError:
+                return val
+
+        return [tryint(chunk) for chunk in re.split('(\d+)', string_to_split)]
diff --git a/Tools/Scripts/webkitpy/layout_tests/controllers/layout_test_runner_unittest.py b/Tools/Scripts/webkitpy/layout_tests/controllers/layout_test_runner_unittest.py
new file mode 100644
index 0000000..4efa4e0
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/controllers/layout_test_runner_unittest.py
@@ -0,0 +1,382 @@
+#!/usr/bin/python
+# Copyright (C) 2012 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
+# 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.host_mock import MockHost
+from webkitpy.layout_tests import run_webkit_tests
+from webkitpy.layout_tests.models import test_expectations
+from webkitpy.layout_tests.models import test_failures
+from webkitpy.layout_tests.models.result_summary import ResultSummary
+from webkitpy.layout_tests.models.test_input import TestInput
+from webkitpy.layout_tests.models.test_results import TestResult
+from webkitpy.layout_tests.controllers.layout_test_runner import LayoutTestRunner, Sharder, TestRunInterruptedException
+
+
+TestExpectations = test_expectations.TestExpectations
+
+
+class FakePrinter(object):
+    num_completed = 0
+    num_tests = 0
+
+    def print_workers_and_shards(self, num_workers, num_shards, num_locked_shards):
+        pass
+
+    def print_started_test(self, test_name):
+        pass
+
+    def print_finished_test(self, result, expected, exp_str, got_str):
+        pass
+
+    def write(self, msg):
+        pass
+
+    def write_update(self, msg):
+        pass
+
+    def flush(self):
+        pass
+
+
+class LockCheckingRunner(LayoutTestRunner):
+    def __init__(self, port, options, printer, tester, http_lock):
+        super(LockCheckingRunner, self).__init__(options, port, printer, port.results_directory(), TestExpectations(port, []), lambda test_name: False)
+        self._finished_list_called = False
+        self._tester = tester
+        self._should_have_http_lock = http_lock
+
+    def handle_finished_list(self, source, list_name, num_tests, elapsed_time):
+        if not self._finished_list_called:
+            self._tester.assertEquals(list_name, 'locked_tests')
+            self._tester.assertTrue(self._remaining_locked_shards)
+            self._tester.assertTrue(self._has_http_lock is self._should_have_http_lock)
+
+        super(LockCheckingRunner, self).handle_finished_list(source, list_name, num_tests, elapsed_time)
+
+        if not self._finished_list_called:
+            self._tester.assertEquals(self._remaining_locked_shards, [])
+            self._tester.assertFalse(self._has_http_lock)
+            self._finished_list_called = True
+
+
+class LayoutTestRunnerTests(unittest.TestCase):
+    def _runner(self, port=None):
+        # FIXME: we shouldn't have to use run_webkit_tests.py to get the options we need.
+        options = run_webkit_tests.parse_args(['--platform', 'test-mac-snowleopard'])[0]
+        options.child_processes = '1'
+
+        host = MockHost()
+        port = port or host.port_factory.get(options.platform, options=options)
+        return LockCheckingRunner(port, options, FakePrinter(), self, True)
+
+    def _result_summary(self, runner, tests):
+        return ResultSummary(TestExpectations(runner._port, tests), tests, 1, set())
+
+    def _run_tests(self, runner, tests):
+        test_inputs = [TestInput(test, 6000) for test in tests]
+        expectations = TestExpectations(runner._port, tests)
+        runner.run_tests(test_inputs, expectations, self._result_summary(runner, tests),
+            num_workers=1, needs_http=any('http' in test for test in tests), needs_websockets=any(['websocket' in test for test in tests]), retrying=False)
+
+    def test_http_locking(self):
+        runner = self._runner()
+        self._run_tests(runner, ['http/tests/passes/text.html', 'passes/text.html'])
+
+    def test_perf_locking(self):
+        runner = self._runner()
+        self._run_tests(runner, ['http/tests/passes/text.html', 'perf/foo/test.html'])
+
+    def test_interrupt_if_at_failure_limits(self):
+        runner = self._runner()
+        runner._options.exit_after_n_failures = None
+        runner._options.exit_after_n_crashes_or_times = None
+        test_names = ['passes/text.html', 'passes/image.html']
+        runner._test_files_list = test_names
+
+        result_summary = self._result_summary(runner, test_names)
+        result_summary.unexpected_failures = 100
+        result_summary.unexpected_crashes = 50
+        result_summary.unexpected_timeouts = 50
+        # No exception when the exit_after* options are None.
+        runner._interrupt_if_at_failure_limits(result_summary)
+
+        # No exception when we haven't hit the limit yet.
+        runner._options.exit_after_n_failures = 101
+        runner._options.exit_after_n_crashes_or_timeouts = 101
+        runner._interrupt_if_at_failure_limits(result_summary)
+
+        # Interrupt if we've exceeded either limit:
+        runner._options.exit_after_n_crashes_or_timeouts = 10
+        self.assertRaises(TestRunInterruptedException, runner._interrupt_if_at_failure_limits, result_summary)
+        self.assertEquals(result_summary.results['passes/text.html'].type, test_expectations.SKIP)
+        self.assertEquals(result_summary.results['passes/image.html'].type, test_expectations.SKIP)
+
+        runner._options.exit_after_n_crashes_or_timeouts = None
+        runner._options.exit_after_n_failures = 10
+        exception = self.assertRaises(TestRunInterruptedException, runner._interrupt_if_at_failure_limits, result_summary)
+
+    def test_update_summary_with_result(self):
+        # Reftests expected to be image mismatch should be respected when pixel_tests=False.
+        runner = self._runner()
+        runner._options.pixel_tests = False
+        test = 'failures/expected/reftest.html'
+        expectations = TestExpectations(runner._port, tests=[test])
+        runner._expectations = expectations
+
+        result_summary = ResultSummary(expectations, [test], 1, set())
+        result = TestResult(test_name=test, failures=[test_failures.FailureReftestMismatchDidNotOccur()], reftest_type=['!='])
+        runner._update_summary_with_result(result_summary, result)
+        self.assertEquals(1, result_summary.expected)
+        self.assertEquals(0, result_summary.unexpected)
+
+        result_summary = ResultSummary(expectations, [test], 1, set())
+        result = TestResult(test_name=test, failures=[], reftest_type=['=='])
+        runner._update_summary_with_result(result_summary, result)
+        self.assertEquals(0, result_summary.expected)
+        self.assertEquals(1, result_summary.unexpected)
+
+    def test_servers_started(self):
+
+        def start_http_server(number_of_servers=None):
+            self.http_started = True
+
+        def start_websocket_server():
+            self.websocket_started = True
+
+        def stop_http_server():
+            self.http_stopped = True
+
+        def stop_websocket_server():
+            self.websocket_stopped = True
+
+        host = MockHost()
+        port = host.port_factory.get('test-mac-leopard')
+        port.start_http_server = start_http_server
+        port.start_websocket_server = start_websocket_server
+        port.stop_http_server = stop_http_server
+        port.stop_websocket_server = stop_websocket_server
+
+        self.http_started = self.http_stopped = self.websocket_started = self.websocket_stopped = False
+        runner = self._runner(port=port)
+        runner._needs_http = True
+        runner._needs_websockets = False
+        runner.start_servers_with_lock(number_of_servers=4)
+        self.assertEquals(self.http_started, True)
+        self.assertEquals(self.websocket_started, False)
+        runner.stop_servers_with_lock()
+        self.assertEquals(self.http_stopped, True)
+        self.assertEquals(self.websocket_stopped, False)
+
+        self.http_started = self.http_stopped = self.websocket_started = self.websocket_stopped = False
+        runner._needs_http = True
+        runner._needs_websockets = True
+        runner.start_servers_with_lock(number_of_servers=4)
+        self.assertEquals(self.http_started, True)
+        self.assertEquals(self.websocket_started, True)
+        runner.stop_servers_with_lock()
+        self.assertEquals(self.http_stopped, True)
+        self.assertEquals(self.websocket_stopped, True)
+
+        self.http_started = self.http_stopped = self.websocket_started = self.websocket_stopped = False
+        runner._needs_http = False
+        runner._needs_websockets = False
+        runner.start_servers_with_lock(number_of_servers=4)
+        self.assertEquals(self.http_started, False)
+        self.assertEquals(self.websocket_started, False)
+        runner.stop_servers_with_lock()
+        self.assertEquals(self.http_stopped, False)
+        self.assertEquals(self.websocket_stopped, False)
+
+
+class SharderTests(unittest.TestCase):
+
+    test_list = [
+        "http/tests/websocket/tests/unicode.htm",
+        "animations/keyframes.html",
+        "http/tests/security/view-source-no-refresh.html",
+        "http/tests/websocket/tests/websocket-protocol-ignored.html",
+        "fast/css/display-none-inline-style-change-crash.html",
+        "http/tests/xmlhttprequest/supported-xml-content-types.html",
+        "dom/html/level2/html/HTMLAnchorElement03.html",
+        "ietestcenter/Javascript/11.1.5_4-4-c-1.html",
+        "dom/html/level2/html/HTMLAnchorElement06.html",
+        "perf/object-keys.html",
+    ]
+
+    def get_test_input(self, test_file):
+        return TestInput(test_file, requires_lock=(test_file.startswith('http') or test_file.startswith('perf')))
+
+    def get_shards(self, num_workers, fully_parallel, test_list=None, max_locked_shards=1):
+        def split(test_name):
+            idx = test_name.rfind('/')
+            if idx != -1:
+                return (test_name[0:idx], test_name[idx + 1:])
+
+        self.sharder = Sharder(split, '/', max_locked_shards)
+        test_list = test_list or self.test_list
+        return self.sharder.shard_tests([self.get_test_input(test) for test in test_list], num_workers, fully_parallel)
+
+    def assert_shards(self, actual_shards, expected_shard_names):
+        self.assertEquals(len(actual_shards), len(expected_shard_names))
+        for i, shard in enumerate(actual_shards):
+            expected_shard_name, expected_test_names = expected_shard_names[i]
+            self.assertEquals(shard.name, expected_shard_name)
+            self.assertEquals([test_input.test_name for test_input in shard.test_inputs],
+                              expected_test_names)
+
+    def test_shard_by_dir(self):
+        locked, unlocked = self.get_shards(num_workers=2, fully_parallel=False)
+
+        # Note that although there are tests in multiple dirs that need locks,
+        # they are crammed into a single shard in order to reduce the # of
+        # workers hitting the server at once.
+        self.assert_shards(locked,
+             [('locked_shard_1',
+               ['http/tests/security/view-source-no-refresh.html',
+                'http/tests/websocket/tests/unicode.htm',
+                'http/tests/websocket/tests/websocket-protocol-ignored.html',
+                'http/tests/xmlhttprequest/supported-xml-content-types.html',
+                'perf/object-keys.html'])])
+        self.assert_shards(unlocked,
+            [('animations', ['animations/keyframes.html']),
+             ('dom/html/level2/html', ['dom/html/level2/html/HTMLAnchorElement03.html',
+                                      'dom/html/level2/html/HTMLAnchorElement06.html']),
+             ('fast/css', ['fast/css/display-none-inline-style-change-crash.html']),
+             ('ietestcenter/Javascript', ['ietestcenter/Javascript/11.1.5_4-4-c-1.html'])])
+
+    def test_shard_every_file(self):
+        locked, unlocked = self.get_shards(num_workers=2, fully_parallel=True)
+        self.assert_shards(locked,
+            [('.', ['http/tests/websocket/tests/unicode.htm']),
+             ('.', ['http/tests/security/view-source-no-refresh.html']),
+             ('.', ['http/tests/websocket/tests/websocket-protocol-ignored.html']),
+             ('.', ['http/tests/xmlhttprequest/supported-xml-content-types.html']),
+             ('.', ['perf/object-keys.html'])]),
+        self.assert_shards(unlocked,
+            [('.', ['animations/keyframes.html']),
+             ('.', ['fast/css/display-none-inline-style-change-crash.html']),
+             ('.', ['dom/html/level2/html/HTMLAnchorElement03.html']),
+             ('.', ['ietestcenter/Javascript/11.1.5_4-4-c-1.html']),
+             ('.', ['dom/html/level2/html/HTMLAnchorElement06.html'])])
+
+    def test_shard_in_two(self):
+        locked, unlocked = self.get_shards(num_workers=1, fully_parallel=False)
+        self.assert_shards(locked,
+            [('locked_tests',
+              ['http/tests/websocket/tests/unicode.htm',
+               'http/tests/security/view-source-no-refresh.html',
+               'http/tests/websocket/tests/websocket-protocol-ignored.html',
+               'http/tests/xmlhttprequest/supported-xml-content-types.html',
+               'perf/object-keys.html'])])
+        self.assert_shards(unlocked,
+            [('unlocked_tests',
+              ['animations/keyframes.html',
+               'fast/css/display-none-inline-style-change-crash.html',
+               'dom/html/level2/html/HTMLAnchorElement03.html',
+               'ietestcenter/Javascript/11.1.5_4-4-c-1.html',
+               'dom/html/level2/html/HTMLAnchorElement06.html'])])
+
+    def test_shard_in_two_has_no_locked_shards(self):
+        locked, unlocked = self.get_shards(num_workers=1, fully_parallel=False,
+             test_list=['animations/keyframe.html'])
+        self.assertEquals(len(locked), 0)
+        self.assertEquals(len(unlocked), 1)
+
+    def test_shard_in_two_has_no_unlocked_shards(self):
+        locked, unlocked = self.get_shards(num_workers=1, fully_parallel=False,
+             test_list=['http/tests/websocket/tests/unicode.htm'])
+        self.assertEquals(len(locked), 1)
+        self.assertEquals(len(unlocked), 0)
+
+    def test_multiple_locked_shards(self):
+        locked, unlocked = self.get_shards(num_workers=4, fully_parallel=False, max_locked_shards=2)
+        self.assert_shards(locked,
+            [('locked_shard_1',
+              ['http/tests/security/view-source-no-refresh.html',
+               'http/tests/websocket/tests/unicode.htm',
+               'http/tests/websocket/tests/websocket-protocol-ignored.html']),
+             ('locked_shard_2',
+              ['http/tests/xmlhttprequest/supported-xml-content-types.html',
+               'perf/object-keys.html'])])
+
+        locked, unlocked = self.get_shards(num_workers=4, fully_parallel=False)
+        self.assert_shards(locked,
+            [('locked_shard_1',
+              ['http/tests/security/view-source-no-refresh.html',
+               'http/tests/websocket/tests/unicode.htm',
+               'http/tests/websocket/tests/websocket-protocol-ignored.html',
+               'http/tests/xmlhttprequest/supported-xml-content-types.html',
+               'perf/object-keys.html'])])
+
+
+class NaturalCompareTest(unittest.TestCase):
+    def assert_cmp(self, x, y, result):
+        self.assertEquals(cmp(Sharder.natural_sort_key(x), Sharder.natural_sort_key(y)), result)
+
+    def test_natural_compare(self):
+        self.assert_cmp('a', 'a', 0)
+        self.assert_cmp('ab', 'a', 1)
+        self.assert_cmp('a', 'ab', -1)
+        self.assert_cmp('', '', 0)
+        self.assert_cmp('', 'ab', -1)
+        self.assert_cmp('1', '2', -1)
+        self.assert_cmp('2', '1', 1)
+        self.assert_cmp('1', '10', -1)
+        self.assert_cmp('2', '10', -1)
+        self.assert_cmp('foo_1.html', 'foo_2.html', -1)
+        self.assert_cmp('foo_1.1.html', 'foo_2.html', -1)
+        self.assert_cmp('foo_1.html', 'foo_10.html', -1)
+        self.assert_cmp('foo_2.html', 'foo_10.html', -1)
+        self.assert_cmp('foo_23.html', 'foo_10.html', 1)
+        self.assert_cmp('foo_23.html', 'foo_100.html', -1)
+
+
+class KeyCompareTest(unittest.TestCase):
+    def setUp(self):
+        def split(test_name):
+            idx = test_name.rfind('/')
+            if idx != -1:
+                return (test_name[0:idx], test_name[idx + 1:])
+
+        self.sharder = Sharder(split, '/', 1)
+
+    def assert_cmp(self, x, y, result):
+        self.assertEquals(cmp(self.sharder.test_key(x), self.sharder.test_key(y)), result)
+
+    def test_test_key(self):
+        self.assert_cmp('/a', '/a', 0)
+        self.assert_cmp('/a', '/b', -1)
+        self.assert_cmp('/a2', '/a10', -1)
+        self.assert_cmp('/a2/foo', '/a10/foo', -1)
+        self.assert_cmp('/a/foo11', '/a/foo2', 1)
+        self.assert_cmp('/ab', '/a/a/b', -1)
+        self.assert_cmp('/a/a/b', '/ab', 1)
+        self.assert_cmp('/foo-bar/baz', '/foo/baz', -1)
diff --git a/Tools/Scripts/webkitpy/layout_tests/controllers/manager.py b/Tools/Scripts/webkitpy/layout_tests/controllers/manager.py
new file mode 100644
index 0000000..636edd2
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/controllers/manager.py
@@ -0,0 +1,612 @@
+#!/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
+# 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.
+
+"""
+The Manager runs a series of tests (TestType interface) against a set
+of test files.  If a test file fails a TestType, it returns a list of TestFailure
+objects to the Manager. The Manager then aggregates the TestFailures to
+create a final report.
+"""
+
+import errno
+import logging
+import math
+import Queue
+import random
+import re
+import sys
+import time
+
+from webkitpy.common import message_pool
+from webkitpy.layout_tests.controllers.layout_test_finder import LayoutTestFinder
+from webkitpy.layout_tests.controllers.layout_test_runner import LayoutTestRunner, TestRunInterruptedException, WorkerException
+from webkitpy.layout_tests.controllers.test_result_writer import TestResultWriter
+from webkitpy.layout_tests.layout_package import json_layout_results_generator
+from webkitpy.layout_tests.layout_package import json_results_generator
+from webkitpy.layout_tests.models import test_expectations
+from webkitpy.layout_tests.models import test_failures
+from webkitpy.layout_tests.models import test_results
+from webkitpy.layout_tests.models.test_input import TestInput
+from webkitpy.layout_tests.models.result_summary import ResultSummary
+from webkitpy.layout_tests.views import printing
+
+from webkitpy.tool import grammar
+
+_log = logging.getLogger(__name__)
+
+# Builder base URL where we have the archived test results.
+BUILDER_BASE_URL = "http://build.chromium.org/buildbot/layout_test_results/"
+
+TestExpectations = test_expectations.TestExpectations
+
+
+def interpret_test_failures(port, test_name, failures):
+    """Interpret test failures and returns a test result as dict.
+
+    Args:
+        port: interface to port-specific hooks
+        test_name: test name relative to layout_tests directory
+        failures: list of test failures
+    Returns:
+        A dictionary like {'is_missing_text': True, ...}
+    """
+    test_dict = {}
+    failure_types = [type(failure) for failure in failures]
+    # FIXME: get rid of all this is_* values once there is a 1:1 map between
+    # TestFailure type and test_expectations.EXPECTATION.
+    if test_failures.FailureMissingAudio in failure_types:
+        test_dict['is_missing_audio'] = True
+
+    if test_failures.FailureMissingResult in failure_types:
+        test_dict['is_missing_text'] = True
+
+    if test_failures.FailureMissingImage in failure_types or test_failures.FailureMissingImageHash in failure_types:
+        test_dict['is_missing_image'] = True
+
+    for failure in failures:
+        if isinstance(failure, test_failures.FailureImageHashMismatch) or isinstance(failure, test_failures.FailureReftestMismatch):
+            test_dict['image_diff_percent'] = failure.diff_percent
+
+    return test_dict
+
+
+def use_trac_links_in_results_html(port_obj):
+    # We only use trac links on the buildbots.
+    # Use existence of builder_name as a proxy for knowing we're on a bot.
+    return port_obj.get_option("builder_name")
+
+
+# FIXME: This should be on the Manager class (since that's the only caller)
+# or split off from Manager onto another helper class, but should not be a free function.
+# Most likely this should be made into its own class, and this super-long function
+# split into many helper functions.
+def summarize_results(port_obj, expectations, result_summary, retry_summary, test_timings, only_unexpected, interrupted):
+    """Summarize failing results as a dict.
+
+    FIXME: split this data structure into a separate class?
+
+    Args:
+        port_obj: interface to port-specific hooks
+        expectations: test_expectations.TestExpectations object
+        result_summary: summary object from initial test runs
+        retry_summary: summary object from final test run of retried tests
+        test_timings: a list of TestResult objects which contain test runtimes in seconds
+        only_unexpected: whether to return a summary only for the unexpected results
+    Returns:
+        A dictionary containing a summary of the unexpected results from the
+        run, with the following fields:
+        'version': a version indicator
+        'fixable': The number of fixable tests (NOW - PASS)
+        'skipped': The number of skipped tests (NOW & SKIPPED)
+        'num_regressions': The number of non-flaky failures
+        'num_flaky': The number of flaky failures
+        'num_missing': The number of tests with missing results
+        'num_passes': The number of unexpected passes
+        'tests': a dict of tests -> {'expected': '...', 'actual': '...'}
+    """
+    results = {}
+    results['version'] = 3
+
+    tbe = result_summary.tests_by_expectation
+    tbt = result_summary.tests_by_timeline
+    results['fixable'] = len(tbt[test_expectations.NOW] - tbe[test_expectations.PASS])
+    results['skipped'] = len(tbt[test_expectations.NOW] & tbe[test_expectations.SKIP])
+
+    num_passes = 0
+    num_flaky = 0
+    num_missing = 0
+    num_regressions = 0
+    keywords = {}
+    for expecation_string, expectation_enum in TestExpectations.EXPECTATIONS.iteritems():
+        keywords[expectation_enum] = expecation_string.upper()
+
+    for modifier_string, modifier_enum in TestExpectations.MODIFIERS.iteritems():
+        keywords[modifier_enum] = modifier_string.upper()
+
+    tests = {}
+    original_results = result_summary.unexpected_results if only_unexpected else result_summary.results
+
+    for test_name, result in original_results.iteritems():
+        # Note that if a test crashed in the original run, we ignore
+        # whether or not it crashed when we retried it (if we retried it),
+        # and always consider the result not flaky.
+        expected = expectations.get_expectations_string(test_name)
+        result_type = result.type
+        actual = [keywords[result_type]]
+
+        if result_type == test_expectations.SKIP:
+            continue
+
+        test_dict = {}
+        if result.has_stderr:
+            test_dict['has_stderr'] = True
+
+        if result.reftest_type:
+            test_dict.update(reftest_type=list(result.reftest_type))
+
+        if expectations.has_modifier(test_name, test_expectations.WONTFIX):
+            test_dict['wontfix'] = True
+
+        if result_type == test_expectations.PASS:
+            num_passes += 1
+            # FIXME: include passing tests that have stderr output.
+            if expected == 'PASS':
+                continue
+        elif result_type == test_expectations.CRASH:
+            num_regressions += 1
+        elif result_type == test_expectations.MISSING:
+            if test_name in result_summary.unexpected_results:
+                num_missing += 1
+        elif test_name in result_summary.unexpected_results:
+            if test_name not in retry_summary.unexpected_results:
+                actual.extend(expectations.get_expectations_string(test_name).split(" "))
+                num_flaky += 1
+            else:
+                retry_result_type = retry_summary.unexpected_results[test_name].type
+                if result_type != retry_result_type:
+                    actual.append(keywords[retry_result_type])
+                    num_flaky += 1
+                else:
+                    num_regressions += 1
+
+        test_dict['expected'] = expected
+        test_dict['actual'] = " ".join(actual)
+        # FIXME: Set this correctly once https://webkit.org/b/37739 is fixed
+        # and only set it if there actually is stderr data.
+
+        test_dict.update(interpret_test_failures(port_obj, test_name, result.failures))
+
+        # Store test hierarchically by directory. e.g.
+        # foo/bar/baz.html: test_dict
+        # foo/bar/baz1.html: test_dict
+        #
+        # becomes
+        # foo: {
+        #     bar: {
+        #         baz.html: test_dict,
+        #         baz1.html: test_dict
+        #     }
+        # }
+        parts = test_name.split('/')
+        current_map = tests
+        for i, part in enumerate(parts):
+            if i == (len(parts) - 1):
+                current_map[part] = test_dict
+                break
+            if part not in current_map:
+                current_map[part] = {}
+            current_map = current_map[part]
+
+    results['tests'] = tests
+    results['num_passes'] = num_passes
+    results['num_flaky'] = num_flaky
+    results['num_missing'] = num_missing
+    results['num_regressions'] = num_regressions
+    results['uses_expectations_file'] = port_obj.uses_test_expectations_file()
+    results['interrupted'] = interrupted  # Does results.html have enough information to compute this itself? (by checking total number of results vs. total number of tests?)
+    results['layout_tests_dir'] = port_obj.layout_tests_dir()
+    results['has_wdiff'] = port_obj.wdiff_available()
+    results['has_pretty_patch'] = port_obj.pretty_patch_available()
+    results['pixel_tests_enabled'] = port_obj.get_option('pixel_tests')
+
+    try:
+        # We only use the svn revision for using trac links in the results.html file,
+        # Don't do this by default since it takes >100ms.
+        # FIXME: Do we really need to populate this both here and in the json_results_generator?
+        if use_trac_links_in_results_html(port_obj):
+            port_obj.host.initialize_scm()
+            results['revision'] = port_obj.host.scm().head_svn_revision()
+    except Exception, e:
+        _log.warn("Failed to determine svn revision for checkout (cwd: %s, webkit_base: %s), leaving 'revision' key blank in full_results.json.\n%s" % (port_obj._filesystem.getcwd(), port_obj.path_from_webkit_base(), e))
+        # Handle cases where we're running outside of version control.
+        import traceback
+        _log.debug('Failed to learn head svn revision:')
+        _log.debug(traceback.format_exc())
+        results['revision'] = ""
+
+    return results
+
+
+class Manager(object):
+    """A class for managing running a series of tests on a series of layout
+    test files."""
+
+    def __init__(self, port, options, printer):
+        """Initialize test runner data structures.
+
+        Args:
+          port: an object implementing port-specific
+          options: a dictionary of command line options
+          printer: a Printer object to record updates to.
+        """
+        self._port = port
+        self._filesystem = port.host.filesystem
+        self._options = options
+        self._printer = printer
+        self._expectations = None
+
+        self.HTTP_SUBDIR = 'http' + port.TEST_PATH_SEPARATOR
+        self.PERF_SUBDIR = 'perf'
+        self.WEBSOCKET_SUBDIR = 'websocket' + port.TEST_PATH_SEPARATOR
+        self.LAYOUT_TESTS_DIRECTORY = 'LayoutTests'
+
+        # disable wss server. need to install pyOpenSSL on buildbots.
+        # self._websocket_secure_server = websocket_server.PyWebSocket(
+        #        options.results_directory, use_tls=True, port=9323)
+
+        self._paths = set()
+        self._test_names = None
+        self._retrying = False
+        self._results_directory = self._port.results_directory()
+        self._finder = LayoutTestFinder(self._port, self._options)
+        self._runner = LayoutTestRunner(self._options, self._port, self._printer, self._results_directory, self._expectations, self._test_is_slow)
+
+    def _collect_tests(self, args):
+        return self._finder.find_tests(self._options, args)
+
+    def _is_http_test(self, test):
+        return self.HTTP_SUBDIR in test or self._is_websocket_test(test)
+
+    def _is_websocket_test(self, test):
+        return self.WEBSOCKET_SUBDIR in test
+
+    def _http_tests(self):
+        return set(test for test in self._test_names if self._is_http_test(test))
+
+    def _websocket_tests(self):
+        return set(test for test in self._test_files if self._is_websocket(test))
+
+    def _is_perf_test(self, test):
+        return self.PERF_SUBDIR == test or (self.PERF_SUBDIR + self._port.TEST_PATH_SEPARATOR) in test
+
+    def _prepare_lists(self):
+        tests_to_skip = self._finder.skip_tests(self._paths, self._test_names, self._expectations, self._http_tests())
+        self._test_names = list(set(self._test_names) - tests_to_skip)
+
+        # Create a sorted list of test files so the subset chunk,
+        # if used, contains alphabetically consecutive tests.
+        if self._options.randomize_order:
+            random.shuffle(self._test_names)
+        else:
+            self._test_names.sort(key=self._runner.test_key)
+
+        self._test_names, tests_in_other_chunks = self._finder.split_into_chunks(self._test_names)
+        self._expectations.add_skipped_tests(tests_in_other_chunks)
+        tests_to_skip.update(tests_in_other_chunks)
+
+        if self._options.repeat_each > 1:
+            list_with_repetitions = []
+            for test in self._test_names:
+                list_with_repetitions += ([test] * self._options.repeat_each)
+            self._test_names = list_with_repetitions
+
+        if self._options.iterations > 1:
+            self._test_names = self._test_names * self._options.iterations
+
+        iterations = self._options.repeat_each * self._options.iterations
+        return ResultSummary(self._expectations, set(self._test_names), iterations, tests_to_skip)
+
+    def _test_input_for_file(self, test_file):
+        return TestInput(test_file,
+            self._options.slow_time_out_ms if self._test_is_slow(test_file) else self._options.time_out_ms,
+            self._test_requires_lock(test_file))
+
+    def _test_requires_lock(self, test_file):
+        """Return True if the test needs to be locked when
+        running multiple copies of NRWTs. Perf tests are locked
+        because heavy load caused by running other tests in parallel
+        might cause some of them to timeout."""
+        return self._is_http_test(test_file) or self._is_perf_test(test_file)
+
+    def _test_is_slow(self, test_file):
+        return self._expectations.has_modifier(test_file, test_expectations.SLOW)
+
+    def needs_servers(self):
+        return any(self._test_requires_lock(test_name) for test_name in self._test_names) and self._options.http
+
+    def _set_up_run(self):
+        self._printer.write_update("Checking build ...")
+        if not self._port.check_build(self.needs_servers()):
+            _log.error("Build check failed")
+            return False
+
+        # This must be started before we check the system dependencies,
+        # since the helper may do things to make the setup correct.
+        if self._options.pixel_tests:
+            self._printer.write_update("Starting pixel test helper ...")
+            self._port.start_helper()
+
+        # Check that the system dependencies (themes, fonts, ...) are correct.
+        if not self._options.nocheck_sys_deps:
+            self._printer.write_update("Checking system dependencies ...")
+            if not self._port.check_sys_deps(self.needs_servers()):
+                self._port.stop_helper()
+                return False
+
+        if self._options.clobber_old_results:
+            self._clobber_old_results()
+
+        # Create the output directory if it doesn't already exist.
+        self._port.host.filesystem.maybe_make_directory(self._results_directory)
+
+        self._port.setup_test_run()
+        return True
+
+    def run(self, args):
+        """Run all our tests on all our test files and return the number of unexpected results (0 == success)."""
+        self._printer.write_update("Collecting tests ...")
+        try:
+            self._paths, self._test_names = self._collect_tests(args)
+        except IOError as exception:
+            # This is raised if --test-list doesn't exist
+            return -1
+
+        self._printer.write_update("Parsing expectations ...")
+        self._expectations = test_expectations.TestExpectations(self._port, self._test_names)
+
+        num_all_test_files_found = len(self._test_names)
+        result_summary = self._prepare_lists()
+
+        # Check to make sure we're not skipping every test.
+        if not self._test_names:
+            _log.critical('No tests to run.')
+            return -1
+
+        self._printer.print_found(num_all_test_files_found, len(self._test_names), self._options.repeat_each, self._options.iterations)
+        self._printer.print_expected(result_summary, self._expectations.get_tests_with_result_type)
+
+        if not self._set_up_run():
+            return -1
+
+        start_time = time.time()
+
+        interrupted, keyboard_interrupted, thread_timings, test_timings, individual_test_timings = \
+            self._run_tests(self._test_names, result_summary, int(self._options.child_processes))
+
+        # We exclude the crashes from the list of results to retry, because
+        # we want to treat even a potentially flaky crash as an error.
+
+        failures = self._get_failures(result_summary, include_crashes=self._port.should_retry_crashes(), include_missing=False)
+        retry_summary = result_summary
+        while (len(failures) and self._options.retry_failures and not self._retrying and not interrupted and not keyboard_interrupted):
+            _log.info('')
+            _log.info("Retrying %d unexpected failure(s) ..." % len(failures))
+            _log.info('')
+            self._retrying = True
+            retry_summary = ResultSummary(self._expectations, failures.keys(), 1, set())
+            # Note that we intentionally ignore the return value here.
+            self._run_tests(failures.keys(), retry_summary, 1)
+            failures = self._get_failures(retry_summary, include_crashes=True, include_missing=True)
+
+        end_time = time.time()
+
+        # Some crash logs can take a long time to be written out so look
+        # for new logs after the test run finishes.
+        self._look_for_new_crash_logs(result_summary, start_time)
+        self._look_for_new_crash_logs(retry_summary, start_time)
+        self._clean_up_run()
+
+        unexpected_results = summarize_results(self._port, self._expectations, result_summary, retry_summary, individual_test_timings, only_unexpected=True, interrupted=interrupted)
+
+        self._printer.print_results(end_time - start_time, thread_timings, test_timings, individual_test_timings, result_summary, unexpected_results)
+
+        # Re-raise a KeyboardInterrupt if necessary so the caller can handle it.
+        if keyboard_interrupted:
+            raise KeyboardInterrupt
+
+        # FIXME: remove record_results. It's just used for testing. There's no need
+        # for it to be a commandline argument.
+        if (self._options.record_results and not self._options.dry_run and not keyboard_interrupted):
+            self._port.print_leaks_summary()
+            # Write the same data to log files and upload generated JSON files to appengine server.
+            summarized_results = summarize_results(self._port, self._expectations, result_summary, retry_summary, individual_test_timings, only_unexpected=False, interrupted=interrupted)
+            self._upload_json_files(summarized_results, result_summary, individual_test_timings)
+
+        # Write the summary to disk (results.html) and display it if requested.
+        if not self._options.dry_run:
+            self._copy_results_html_file()
+            if self._options.show_results:
+                self._show_results_html_file(result_summary)
+
+        return self._port.exit_code_from_summarized_results(unexpected_results)
+
+    def _run_tests(self, tests, result_summary, num_workers):
+        test_inputs = [self._test_input_for_file(test) for test in tests]
+        needs_http = self._port.requires_http_server() or any(self._is_http_test(test) for test in tests)
+        needs_websockets = any(self._is_websocket_test(test) for test in tests)
+        return self._runner.run_tests(test_inputs, self._expectations, result_summary, num_workers, needs_http, needs_websockets, self._retrying)
+
+    def _clean_up_run(self):
+        """Restores the system after we're done running tests."""
+        _log.debug("flushing stdout")
+        sys.stdout.flush()
+        _log.debug("flushing stderr")
+        sys.stderr.flush()
+        _log.debug("stopping helper")
+        self._port.stop_helper()
+        _log.debug("cleaning up port")
+        self._port.clean_up_test_run()
+
+    def _look_for_new_crash_logs(self, result_summary, start_time):
+        """Since crash logs can take a long time to be written out if the system is
+           under stress do a second pass at the end of the test run.
+
+           result_summary: the results of the test run
+           start_time: time the tests started at.  We're looking for crash
+               logs after that time.
+        """
+        crashed_processes = []
+        for test, result in result_summary.unexpected_results.iteritems():
+            if (result.type != test_expectations.CRASH):
+                continue
+            for failure in result.failures:
+                if not isinstance(failure, test_failures.FailureCrash):
+                    continue
+                crashed_processes.append([test, failure.process_name, failure.pid])
+
+        crash_logs = self._port.look_for_new_crash_logs(crashed_processes, start_time)
+        if crash_logs:
+            for test, crash_log in crash_logs.iteritems():
+                writer = TestResultWriter(self._port._filesystem, self._port, self._port.results_directory(), test)
+                writer.write_crash_log(crash_log)
+
+    def _clobber_old_results(self):
+        # Just clobber the actual test results directories since the other
+        # files in the results directory are explicitly used for cross-run
+        # tracking.
+        self._printer.write_update("Clobbering old results in %s" %
+                                   self._results_directory)
+        layout_tests_dir = self._port.layout_tests_dir()
+        possible_dirs = self._port.test_dirs()
+        for dirname in possible_dirs:
+            if self._filesystem.isdir(self._filesystem.join(layout_tests_dir, dirname)):
+                self._filesystem.rmtree(self._filesystem.join(self._results_directory, dirname))
+
+    def _get_failures(self, result_summary, include_crashes, include_missing):
+        """Filters a dict of results and returns only the failures.
+
+        Args:
+          result_summary: the results of the test run
+          include_crashes: whether crashes are included in the output.
+            We use False when finding the list of failures to retry
+            to see if the results were flaky. Although the crashes may also be
+            flaky, we treat them as if they aren't so that they're not ignored.
+        Returns:
+          a dict of files -> results
+        """
+        failed_results = {}
+        for test, result in result_summary.unexpected_results.iteritems():
+            if (result.type == test_expectations.PASS or
+                (result.type == test_expectations.CRASH and not include_crashes) or
+                (result.type == test_expectations.MISSING and not include_missing)):
+                continue
+            failed_results[test] = result.type
+
+        return failed_results
+
+    def _char_for_result(self, result):
+        result = result.lower()
+        if result in TestExpectations.EXPECTATIONS:
+            result_enum_value = TestExpectations.EXPECTATIONS[result]
+        else:
+            result_enum_value = TestExpectations.MODIFIERS[result]
+        return json_layout_results_generator.JSONLayoutResultsGenerator.FAILURE_TO_CHAR[result_enum_value]
+
+    def _upload_json_files(self, summarized_results, result_summary, individual_test_timings):
+        """Writes the results of the test run as JSON files into the results
+        dir and upload the files to the appengine server.
+
+        Args:
+          unexpected_results: dict of unexpected results
+          summarized_results: dict of results
+          result_summary: full summary object
+          individual_test_timings: list of test times (used by the flakiness
+            dashboard).
+        """
+        _log.debug("Writing JSON files in %s." % self._results_directory)
+
+        times_trie = json_results_generator.test_timings_trie(self._port, individual_test_timings)
+        times_json_path = self._filesystem.join(self._results_directory, "times_ms.json")
+        json_results_generator.write_json(self._filesystem, times_trie, times_json_path)
+
+        full_results_path = self._filesystem.join(self._results_directory, "full_results.json")
+        # We write full_results.json out as jsonp because we need to load it from a file url and Chromium doesn't allow that.
+        json_results_generator.write_json(self._filesystem, summarized_results, full_results_path, callback="ADD_RESULTS")
+
+        generator = json_layout_results_generator.JSONLayoutResultsGenerator(
+            self._port, self._options.builder_name, self._options.build_name,
+            self._options.build_number, self._results_directory,
+            BUILDER_BASE_URL, individual_test_timings,
+            self._expectations, result_summary, self._test_names,
+            self._options.test_results_server,
+            "layout-tests",
+            self._options.master_name)
+
+        _log.debug("Finished writing JSON files.")
+
+        json_files = ["incremental_results.json", "full_results.json", "times_ms.json"]
+
+        generator.upload_json_files(json_files)
+
+        incremental_results_path = self._filesystem.join(self._results_directory, "incremental_results.json")
+
+        # Remove these files from the results directory so they don't take up too much space on the buildbot.
+        # The tools use the version we uploaded to the results server anyway.
+        self._filesystem.remove(times_json_path)
+        self._filesystem.remove(incremental_results_path)
+
+    def _num_digits(self, num):
+        """Returns the number of digits needed to represent the length of a
+        sequence."""
+        ndigits = 1
+        if len(num):
+            ndigits = int(math.log10(len(num))) + 1
+        return ndigits
+
+    def _copy_results_html_file(self):
+        base_dir = self._port.path_from_webkit_base('LayoutTests', 'fast', 'harness')
+        results_file = self._filesystem.join(base_dir, 'results.html')
+        # FIXME: What should we do if this doesn't exist (e.g., in unit tests)?
+        if self._filesystem.exists(results_file):
+            self._filesystem.copyfile(results_file, self._filesystem.join(self._results_directory, "results.html"))
+
+    def _show_results_html_file(self, result_summary):
+        """Shows the results.html page."""
+        if self._options.full_results_html:
+            test_files = result_summary.failures.keys()
+        else:
+            unexpected_failures = self._get_failures(result_summary, include_crashes=True, include_missing=True)
+            test_files = unexpected_failures.keys()
+
+        if not len(test_files):
+            return
+
+        results_filename = self._filesystem.join(self._results_directory, "results.html")
+        self._port.show_results_html_file(results_filename)
diff --git a/Tools/Scripts/webkitpy/layout_tests/controllers/manager_unittest.py b/Tools/Scripts/webkitpy/layout_tests/controllers/manager_unittest.py
new file mode 100644
index 0000000..e94d133
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/controllers/manager_unittest.py
@@ -0,0 +1,204 @@
+#!/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
+# 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.
+
+"""Unit tests for manager.py."""
+
+import sys
+import time
+import unittest
+
+from webkitpy.common.host_mock import MockHost
+from webkitpy.layout_tests.controllers.manager import Manager, interpret_test_failures, summarize_results
+from webkitpy.layout_tests.models import test_expectations
+from webkitpy.layout_tests.models import test_failures
+from webkitpy.layout_tests.models import test_results
+from webkitpy.layout_tests.models.result_summary import ResultSummary
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.mocktool import MockOptions
+
+
+class ManagerTest(unittest.TestCase):
+    def test_needs_servers(self):
+        def get_manager_with_tests(test_names):
+            port = Mock()  # FIXME: Use a tighter mock.
+            port.TEST_PATH_SEPARATOR = '/'
+            manager = Manager(port, options=MockOptions(http=True, max_locked_shards=1), printer=Mock())
+            manager._test_names = test_names
+            return manager
+
+        manager = get_manager_with_tests(['fast/html'])
+        self.assertFalse(manager.needs_servers())
+
+        manager = get_manager_with_tests(['http/tests/misc'])
+        self.assertTrue(manager.needs_servers())
+
+    def integration_test_needs_servers(self):
+        def get_manager_with_tests(test_names):
+            host = MockHost()
+            port = host.port_factory.get()
+            manager = Manager(port, options=MockOptions(test_list=None, http=True, max_locked_shards=1), printer=Mock())
+            manager._collect_tests(test_names)
+            return manager
+
+        manager = get_manager_with_tests(['fast/html'])
+        self.assertFalse(manager.needs_servers())
+
+        manager = get_manager_with_tests(['http/tests/mime'])
+        self.assertTrue(manager.needs_servers())
+
+        if sys.platform == 'win32':
+            manager = get_manager_with_tests(['fast\\html'])
+            self.assertFalse(manager.needs_servers())
+
+            manager = get_manager_with_tests(['http\\tests\\mime'])
+            self.assertTrue(manager.needs_servers())
+
+    def test_look_for_new_crash_logs(self):
+        def get_manager_with_tests(test_names):
+            host = MockHost()
+            port = host.port_factory.get('test-mac-leopard')
+            manager = Manager(port, options=MockOptions(test_list=None, http=True, max_locked_shards=1), printer=Mock())
+            manager._collect_tests(test_names)
+            return manager
+        host = MockHost()
+        port = host.port_factory.get('test-mac-leopard')
+        tests = ['failures/expected/crash.html']
+        expectations = test_expectations.TestExpectations(port, tests)
+        rs = ResultSummary(expectations, tests, 1, set())
+        manager = get_manager_with_tests(tests)
+        manager._look_for_new_crash_logs(rs, time.time())
+
+
+class ResultSummaryTest(unittest.TestCase):
+
+    def setUp(self):
+        host = MockHost()
+        self.port = host.port_factory.get(port_name='test')
+
+    def test_interpret_test_failures(self):
+        test_dict = interpret_test_failures(self.port, 'foo/reftest.html',
+            [test_failures.FailureImageHashMismatch(diff_percent=0.42)])
+        self.assertEqual(test_dict['image_diff_percent'], 0.42)
+
+        test_dict = interpret_test_failures(self.port, 'foo/reftest.html',
+            [test_failures.FailureReftestMismatch(self.port.abspath_for_test('foo/reftest-expected.html'))])
+        self.assertTrue('image_diff_percent' in test_dict)
+
+        test_dict = interpret_test_failures(self.port, 'foo/reftest.html',
+            [test_failures.FailureReftestMismatchDidNotOccur(self.port.abspath_for_test('foo/reftest-expected-mismatch.html'))])
+        self.assertEqual(len(test_dict), 0)
+
+        test_dict = interpret_test_failures(self.port, 'foo/audio-test.html',
+            [test_failures.FailureMissingAudio()])
+        self.assertTrue('is_missing_audio' in test_dict)
+
+        test_dict = interpret_test_failures(self.port, 'foo/text-test.html',
+            [test_failures.FailureMissingResult()])
+        self.assertTrue('is_missing_text' in test_dict)
+
+        test_dict = interpret_test_failures(self.port, 'foo/pixel-test.html',
+            [test_failures.FailureMissingImage()])
+        self.assertTrue('is_missing_image' in test_dict)
+
+        test_dict = interpret_test_failures(self.port, 'foo/pixel-test.html',
+            [test_failures.FailureMissingImageHash()])
+        self.assertTrue('is_missing_image' in test_dict)
+
+    def get_result(self, test_name, result_type=test_expectations.PASS, run_time=0):
+        failures = []
+        if result_type == test_expectations.TIMEOUT:
+            failures = [test_failures.FailureTimeout()]
+        elif result_type == test_expectations.CRASH:
+            failures = [test_failures.FailureCrash()]
+        return test_results.TestResult(test_name, failures=failures, test_run_time=run_time)
+
+    def get_result_summary(self, port, test_names, expectations_str):
+        port.expectations_dict = lambda: {'': expectations_str}
+        expectations = test_expectations.TestExpectations(port, test_names)
+        return test_names, ResultSummary(expectations, test_names, 1, set()), expectations
+
+    # FIXME: Use this to test more of summarize_results. This was moved from printing_unittest.py.
+    def summarized_results(self, port, expected, passing, flaky, extra_tests=[], extra_expectations=None):
+        tests = ['passes/text.html', 'failures/expected/timeout.html', 'failures/expected/crash.html', 'failures/expected/wontfix.html']
+        if extra_tests:
+            tests.extend(extra_tests)
+
+        expectations = ''
+        if extra_expectations:
+            expectations += extra_expectations
+
+        test_is_slow = False
+        paths, rs, exp = self.get_result_summary(port, tests, expectations)
+        if expected:
+            rs.add(self.get_result('passes/text.html', test_expectations.PASS), expected, test_is_slow)
+            rs.add(self.get_result('failures/expected/timeout.html', test_expectations.TIMEOUT), expected, test_is_slow)
+            rs.add(self.get_result('failures/expected/crash.html', test_expectations.CRASH), expected, test_is_slow)
+        elif passing:
+            rs.add(self.get_result('passes/text.html'), expected, test_is_slow)
+            rs.add(self.get_result('failures/expected/timeout.html'), expected, test_is_slow)
+            rs.add(self.get_result('failures/expected/crash.html'), expected, test_is_slow)
+        else:
+            rs.add(self.get_result('passes/text.html', test_expectations.TIMEOUT), expected, test_is_slow)
+            rs.add(self.get_result('failures/expected/timeout.html', test_expectations.CRASH), expected, test_is_slow)
+            rs.add(self.get_result('failures/expected/crash.html', test_expectations.TIMEOUT), expected, test_is_slow)
+
+        for test in extra_tests:
+            rs.add(self.get_result(test, test_expectations.CRASH), expected, test_is_slow)
+
+        retry = rs
+        if flaky:
+            paths, retry, exp = self.get_result_summary(port, tests, expectations)
+            retry.add(self.get_result('passes/text.html'), True, test_is_slow)
+            retry.add(self.get_result('failures/expected/timeout.html'), True, test_is_slow)
+            retry.add(self.get_result('failures/expected/crash.html'), True, test_is_slow)
+        unexpected_results = summarize_results(port, exp, rs, retry, test_timings={}, only_unexpected=True, interrupted=False)
+        expected_results = summarize_results(port, exp, rs, retry, test_timings={}, only_unexpected=False, interrupted=False)
+        return expected_results, unexpected_results
+
+    def test_no_svn_revision(self):
+        host = MockHost(initialize_scm_by_default=False)
+        port = host.port_factory.get('test')
+        expected_results, unexpected_results = self.summarized_results(port, expected=False, passing=False, flaky=False)
+        self.assertTrue('revision' not in unexpected_results)
+
+    def test_svn_revision(self):
+        host = MockHost(initialize_scm_by_default=False)
+        port = host.port_factory.get('test')
+        port._options.builder_name = 'dummy builder'
+        expected_results, unexpected_results = self.summarized_results(port, expected=False, passing=False, flaky=False)
+        self.assertNotEquals(unexpected_results['revision'], '')
+
+    def test_summarized_results_wontfix(self):
+        host = MockHost()
+        port = host.port_factory.get('test')
+        port._options.builder_name = 'dummy builder'
+        port._filesystem.write_text_file(port._filesystem.join(port.layout_tests_dir(), "failures/expected/wontfix.html"), "Dummy test contents")
+        expected_results, unexpected_results = self.summarized_results(port, expected=False, passing=False, flaky=False, extra_tests=['failures/expected/wontfix.html'], extra_expectations='Bug(x) failures/expected/wontfix.html [ WontFix ]\n')
+        self.assertTrue(expected_results['tests']['failures']['expected']['wontfix.html']['wontfix'])
diff --git a/Tools/Scripts/webkitpy/layout_tests/controllers/single_test_runner.py b/Tools/Scripts/webkitpy/layout_tests/controllers/single_test_runner.py
new file mode 100644
index 0000000..28e9d63
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/controllers/single_test_runner.py
@@ -0,0 +1,339 @@
+# Copyright (C) 2011 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 logging
+import re
+import time
+
+from webkitpy.layout_tests.controllers import test_result_writer
+from webkitpy.layout_tests.port.driver import DriverInput, DriverOutput
+from webkitpy.layout_tests.models import test_expectations
+from webkitpy.layout_tests.models import test_failures
+from webkitpy.layout_tests.models.test_results import TestResult
+
+
+_log = logging.getLogger(__name__)
+
+
+def run_single_test(port, options, test_input, driver, worker_name, stop_when_done):
+    runner = SingleTestRunner(options, port, driver, test_input, worker_name, stop_when_done)
+    return runner.run()
+
+
+class SingleTestRunner(object):
+    (ALONGSIDE_TEST, PLATFORM_DIR, VERSION_DIR, UPDATE) = ('alongside', 'platform', 'version', 'update')
+
+    def __init__(self, options, port, driver, test_input, worker_name, stop_when_done):
+        self._options = options
+        self._port = port
+        self._filesystem = port.host.filesystem
+        self._driver = driver
+        self._timeout = test_input.timeout
+        self._worker_name = worker_name
+        self._test_name = test_input.test_name
+        self._should_run_pixel_test = test_input.should_run_pixel_test
+        self._reference_files = test_input.reference_files
+        self._stop_when_done = stop_when_done
+
+        if self._reference_files:
+            # Detect and report a test which has a wrong combination of expectation files.
+            # For example, if 'foo.html' has two expectation files, 'foo-expected.html' and
+            # 'foo-expected.txt', we should warn users. One test file must be used exclusively
+            # in either layout tests or reftests, but not in both.
+            for suffix in ('.txt', '.png', '.wav'):
+                expected_filename = self._port.expected_filename(self._test_name, suffix)
+                if self._filesystem.exists(expected_filename):
+                    _log.error('%s is a reftest, but has an unused expectation file. Please remove %s.',
+                        self._test_name, expected_filename)
+
+    def _expected_driver_output(self):
+        return DriverOutput(self._port.expected_text(self._test_name),
+                                 self._port.expected_image(self._test_name),
+                                 self._port.expected_checksum(self._test_name),
+                                 self._port.expected_audio(self._test_name))
+
+    def _should_fetch_expected_checksum(self):
+        return self._should_run_pixel_test and not (self._options.new_baseline or self._options.reset_results)
+
+    def _driver_input(self):
+        # The image hash is used to avoid doing an image dump if the
+        # checksums match, so it should be set to a blank value if we
+        # are generating a new baseline.  (Otherwise, an image from a
+        # previous run will be copied into the baseline."""
+        image_hash = None
+        if self._should_fetch_expected_checksum():
+            image_hash = self._port.expected_checksum(self._test_name)
+        return DriverInput(self._test_name, self._timeout, image_hash, self._should_run_pixel_test)
+
+    def run(self):
+        if self._reference_files:
+            if self._port.get_option('no_ref_tests') or self._options.reset_results:
+                reftest_type = set([reference_file[0] for reference_file in self._reference_files])
+                result = TestResult(self._test_name, reftest_type=reftest_type)
+                result.type = test_expectations.SKIP
+                return result
+            return self._run_reftest()
+        if self._options.reset_results:
+            return self._run_rebaseline()
+        return self._run_compare_test()
+
+    def _run_compare_test(self):
+        driver_output = self._driver.run_test(self._driver_input(), self._stop_when_done)
+        expected_driver_output = self._expected_driver_output()
+
+        if self._options.ignore_metrics:
+            expected_driver_output.strip_metrics()
+            driver_output.strip_metrics()
+
+        test_result = self._compare_output(expected_driver_output, driver_output)
+        if self._options.new_test_results:
+            self._add_missing_baselines(test_result, driver_output)
+        test_result_writer.write_test_result(self._filesystem, self._port, self._test_name, driver_output, expected_driver_output, test_result.failures)
+        return test_result
+
+    def _run_rebaseline(self):
+        driver_output = self._driver.run_test(self._driver_input(), self._stop_when_done)
+        failures = self._handle_error(driver_output)
+        test_result_writer.write_test_result(self._filesystem, self._port, self._test_name, driver_output, None, failures)
+        # FIXME: It the test crashed or timed out, it might be better to avoid
+        # to write new baselines.
+        self._overwrite_baselines(driver_output)
+        return TestResult(self._test_name, failures, driver_output.test_time, driver_output.has_stderr())
+
+    _render_tree_dump_pattern = re.compile(r"^layer at \(\d+,\d+\) size \d+x\d+\n")
+
+    def _add_missing_baselines(self, test_result, driver_output):
+        missingImage = test_result.has_failure_matching_types(test_failures.FailureMissingImage, test_failures.FailureMissingImageHash)
+        if test_result.has_failure_matching_types(test_failures.FailureMissingResult):
+            self._save_baseline_data(driver_output.text, '.txt', self._location_for_new_baseline(driver_output.text, '.txt'))
+        if test_result.has_failure_matching_types(test_failures.FailureMissingAudio):
+            self._save_baseline_data(driver_output.audio, '.wav', self._location_for_new_baseline(driver_output.audio, '.wav'))
+        if missingImage:
+            self._save_baseline_data(driver_output.image, '.png', self._location_for_new_baseline(driver_output.image, '.png'))
+
+    def _location_for_new_baseline(self, data, extension):
+        if self._options.add_platform_exceptions:
+            return self.VERSION_DIR
+        if extension == '.png':
+            return self.PLATFORM_DIR
+        if extension == '.wav':
+            return self.ALONGSIDE_TEST
+        if extension == '.txt' and self._render_tree_dump_pattern.match(data):
+            return self.PLATFORM_DIR
+        return self.ALONGSIDE_TEST
+
+    def _overwrite_baselines(self, driver_output):
+        location = self.VERSION_DIR if self._options.add_platform_exceptions else self.UPDATE
+        self._save_baseline_data(driver_output.text, '.txt', location)
+        self._save_baseline_data(driver_output.audio, '.wav', location)
+        if self._should_run_pixel_test:
+            self._save_baseline_data(driver_output.image, '.png', location)
+
+    def _save_baseline_data(self, data, extension, location):
+        if data is None:
+            return
+        port = self._port
+        fs = self._filesystem
+        if location == self.ALONGSIDE_TEST:
+            output_dir = fs.dirname(port.abspath_for_test(self._test_name))
+        elif location == self.VERSION_DIR:
+            output_dir = fs.join(port.baseline_version_dir(), fs.dirname(self._test_name))
+        elif location == self.PLATFORM_DIR:
+            output_dir = fs.join(port.baseline_platform_dir(), fs.dirname(self._test_name))
+        elif location == self.UPDATE:
+            output_dir = fs.dirname(port.expected_filename(self._test_name, extension))
+        else:
+            raise AssertionError('unrecognized baseline location: %s' % location)
+
+        fs.maybe_make_directory(output_dir)
+        output_basename = fs.basename(fs.splitext(self._test_name)[0] + "-expected" + extension)
+        output_path = fs.join(output_dir, output_basename)
+        _log.info('Writing new expected result "%s"' % port.relative_test_filename(output_path))
+        port.update_baseline(output_path, data)
+
+    def _handle_error(self, driver_output, reference_filename=None):
+        """Returns test failures if some unusual errors happen in driver's run.
+
+        Args:
+          driver_output: The output from the driver.
+          reference_filename: The full path to the reference file which produced the driver_output.
+              This arg is optional and should be used only in reftests until we have a better way to know
+              which html file is used for producing the driver_output.
+        """
+        failures = []
+        fs = self._filesystem
+        if driver_output.timeout:
+            failures.append(test_failures.FailureTimeout(bool(reference_filename)))
+
+        if reference_filename:
+            testname = self._port.relative_test_filename(reference_filename)
+        else:
+            testname = self._test_name
+
+        if driver_output.crash:
+            failures.append(test_failures.FailureCrash(bool(reference_filename),
+                                                       driver_output.crashed_process_name,
+                                                       driver_output.crashed_pid))
+            if driver_output.error:
+                _log.debug("%s %s crashed, (stderr lines):" % (self._worker_name, testname))
+            else:
+                _log.debug("%s %s crashed, (no stderr)" % (self._worker_name, testname))
+        elif driver_output.error:
+            _log.debug("%s %s output stderr lines:" % (self._worker_name, testname))
+        for line in driver_output.error.splitlines():
+            _log.debug("  %s" % line)
+        return failures
+
+    def _compare_output(self, expected_driver_output, driver_output):
+        failures = []
+        failures.extend(self._handle_error(driver_output))
+
+        if driver_output.crash:
+            # Don't continue any more if we already have a crash.
+            # In case of timeouts, we continue since we still want to see the text and image output.
+            return TestResult(self._test_name, failures, driver_output.test_time, driver_output.has_stderr())
+
+        failures.extend(self._compare_text(expected_driver_output.text, driver_output.text))
+        failures.extend(self._compare_audio(expected_driver_output.audio, driver_output.audio))
+        if self._should_run_pixel_test:
+            failures.extend(self._compare_image(expected_driver_output, driver_output))
+        return TestResult(self._test_name, failures, driver_output.test_time, driver_output.has_stderr())
+
+    def _compare_text(self, expected_text, actual_text):
+        failures = []
+        if (expected_text and actual_text and
+            # Assuming expected_text is already normalized.
+            self._port.do_text_results_differ(expected_text, self._get_normalized_output_text(actual_text))):
+            failures.append(test_failures.FailureTextMismatch())
+        elif actual_text and not expected_text:
+            failures.append(test_failures.FailureMissingResult())
+        return failures
+
+    def _compare_audio(self, expected_audio, actual_audio):
+        failures = []
+        if (expected_audio and actual_audio and
+            self._port.do_audio_results_differ(expected_audio, actual_audio)):
+            failures.append(test_failures.FailureAudioMismatch())
+        elif actual_audio and not expected_audio:
+            failures.append(test_failures.FailureMissingAudio())
+        return failures
+
+    def _get_normalized_output_text(self, output):
+        """Returns the normalized text output, i.e. the output in which
+        the end-of-line characters are normalized to "\n"."""
+        # Running tests on Windows produces "\r\n".  The "\n" part is helpfully
+        # changed to "\r\n" by our system (Python/Cygwin), resulting in
+        # "\r\r\n", when, in fact, we wanted to compare the text output with
+        # the normalized text expectation files.
+        return output.replace("\r\r\n", "\r\n").replace("\r\n", "\n")
+
+    # FIXME: This function also creates the image diff. Maybe that work should
+    # be handled elsewhere?
+    def _compare_image(self, expected_driver_output, driver_output):
+        failures = []
+        # If we didn't produce a hash file, this test must be text-only.
+        if driver_output.image_hash is None:
+            return failures
+        if not expected_driver_output.image:
+            failures.append(test_failures.FailureMissingImage())
+        elif not expected_driver_output.image_hash:
+            failures.append(test_failures.FailureMissingImageHash())
+        elif driver_output.image_hash != expected_driver_output.image_hash:
+            diff_result = self._port.diff_image(expected_driver_output.image, driver_output.image)
+            err_str = diff_result[2]
+            if err_str:
+                _log.warning('  %s : %s' % (self._test_name, err_str))
+                failures.append(test_failures.FailureImageHashMismatch())
+                driver_output.error = (driver_output.error or '') + err_str
+            else:
+                driver_output.image_diff = diff_result[0]
+                if driver_output.image_diff:
+                    failures.append(test_failures.FailureImageHashMismatch(diff_result[1]))
+                else:
+                    # See https://bugs.webkit.org/show_bug.cgi?id=69444 for why this isn't a full failure.
+                    _log.warning('  %s -> pixel hash failed (but diff passed)' % self._test_name)
+        return failures
+
+    def _run_reftest(self):
+        test_output = self._driver.run_test(self._driver_input(), self._stop_when_done)
+        total_test_time = 0
+        reference_output = None
+        test_result = None
+
+        # A reftest can have multiple match references and multiple mismatch references;
+        # the test fails if any mismatch matches and all of the matches don't match.
+        # To minimize the number of references we have to check, we run all of the mismatches first,
+        # then the matches, and short-circuit out as soon as we can.
+        # Note that sorting by the expectation sorts "!=" before "==" so this is easy to do.
+
+        putAllMismatchBeforeMatch = sorted
+        for expectation, reference_filename in putAllMismatchBeforeMatch(self._reference_files):
+            reference_test_name = self._port.relative_test_filename(reference_filename)
+            reference_output = self._driver.run_test(DriverInput(reference_test_name, self._timeout, None, should_run_pixel_test=True), self._stop_when_done)
+            test_result = self._compare_output_with_reference(reference_output, test_output, reference_filename, expectation == '!=')
+
+            if (expectation == '!=' and test_result.failures) or (expectation == '==' and not test_result.failures):
+                break
+            total_test_time += test_result.test_run_time
+
+        assert(reference_output)
+        test_result_writer.write_test_result(self._filesystem, self._port, self._test_name, test_output, reference_output, test_result.failures)
+        reftest_type = set([reference_file[0] for reference_file in self._reference_files])
+        return TestResult(self._test_name, test_result.failures, total_test_time + test_result.test_run_time, test_result.has_stderr, reftest_type=reftest_type)
+
+    def _compare_output_with_reference(self, reference_driver_output, actual_driver_output, reference_filename, mismatch):
+        total_test_time = reference_driver_output.test_time + actual_driver_output.test_time
+        has_stderr = reference_driver_output.has_stderr() or actual_driver_output.has_stderr()
+        failures = []
+        failures.extend(self._handle_error(actual_driver_output))
+        if failures:
+            # Don't continue any more if we already have crash or timeout.
+            return TestResult(self._test_name, failures, total_test_time, has_stderr)
+        failures.extend(self._handle_error(reference_driver_output, reference_filename=reference_filename))
+        if failures:
+            return TestResult(self._test_name, failures, total_test_time, has_stderr)
+
+        if not reference_driver_output.image_hash and not actual_driver_output.image_hash:
+            failures.append(test_failures.FailureReftestNoImagesGenerated(reference_filename))
+        elif mismatch:
+            if reference_driver_output.image_hash == actual_driver_output.image_hash:
+                diff_result = self._port.diff_image(reference_driver_output.image, actual_driver_output.image, tolerance=0)
+                if not diff_result[0]:
+                    failures.append(test_failures.FailureReftestMismatchDidNotOccur(reference_filename))
+                else:
+                    _log.warning("  %s -> ref test hashes matched but diff failed" % self._test_name)
+
+        elif reference_driver_output.image_hash != actual_driver_output.image_hash:
+            diff_result = self._port.diff_image(reference_driver_output.image, actual_driver_output.image, tolerance=0)
+            if diff_result[0]:
+                failures.append(test_failures.FailureReftestMismatch(reference_filename))
+            else:
+                _log.warning("  %s -> ref test hashes didn't match but diff passed" % self._test_name)
+
+        return TestResult(self._test_name, failures, total_test_time, has_stderr)
diff --git a/Tools/Scripts/webkitpy/layout_tests/controllers/test_result_writer.py b/Tools/Scripts/webkitpy/layout_tests/controllers/test_result_writer.py
new file mode 100644
index 0000000..be178ab
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/controllers/test_result_writer.py
@@ -0,0 +1,269 @@
+# Copyright (C) 2011 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 logging
+
+from webkitpy.layout_tests.models import test_failures
+
+
+_log = logging.getLogger(__name__)
+
+
+def write_test_result(filesystem, port, test_name, driver_output,
+                      expected_driver_output, failures):
+    """Write the test result to the result output directory."""
+    root_output_dir = port.results_directory()
+    writer = TestResultWriter(filesystem, port, root_output_dir, test_name)
+
+    if driver_output.error:
+        writer.write_stderr(driver_output.error)
+
+    for failure in failures:
+        # FIXME: Instead of this long 'if' block, each failure class might
+        # have a responsibility for writing a test result.
+        if isinstance(failure, (test_failures.FailureMissingResult,
+                                test_failures.FailureTextMismatch)):
+            writer.write_text_files(driver_output.text, expected_driver_output.text)
+            writer.create_text_diff_and_write_result(driver_output.text, expected_driver_output.text)
+        elif isinstance(failure, test_failures.FailureMissingImage):
+            writer.write_image_files(driver_output.image, expected_image=None)
+        elif isinstance(failure, test_failures.FailureMissingImageHash):
+            writer.write_image_files(driver_output.image, expected_driver_output.image)
+        elif isinstance(failure, test_failures.FailureImageHashMismatch):
+            writer.write_image_files(driver_output.image, expected_driver_output.image)
+            writer.write_image_diff_files(driver_output.image_diff)
+        elif isinstance(failure, (test_failures.FailureAudioMismatch,
+                                  test_failures.FailureMissingAudio)):
+            writer.write_audio_files(driver_output.audio, expected_driver_output.audio)
+        elif isinstance(failure, test_failures.FailureCrash):
+            crashed_driver_output = expected_driver_output if failure.is_reftest else driver_output
+            writer.write_crash_log(crashed_driver_output.crash_log)
+        elif isinstance(failure, test_failures.FailureReftestMismatch):
+            writer.write_image_files(driver_output.image, expected_driver_output.image)
+            # FIXME: This work should be done earlier in the pipeline (e.g., when we compare images for non-ref tests).
+            # FIXME: We should always have 2 images here.
+            if driver_output.image and expected_driver_output.image:
+                diff_image, diff_percent, err_str = port.diff_image(expected_driver_output.image, driver_output.image, tolerance=0)
+                if diff_image:
+                    writer.write_image_diff_files(diff_image)
+                    failure.diff_percent = diff_percent
+                else:
+                    _log.warn('ref test mismatch did not produce an image diff.')
+            writer.write_reftest(failure.reference_filename)
+        elif isinstance(failure, test_failures.FailureReftestMismatchDidNotOccur):
+            writer.write_image_files(driver_output.image, expected_image=None)
+            writer.write_reftest(failure.reference_filename)
+        else:
+            assert isinstance(failure, (test_failures.FailureTimeout, test_failures.FailureReftestNoImagesGenerated))
+
+
+class TestResultWriter(object):
+    """A class which handles all writing operations to the result directory."""
+
+    # Filename pieces when writing failures to the test results directory.
+    FILENAME_SUFFIX_ACTUAL = "-actual"
+    FILENAME_SUFFIX_EXPECTED = "-expected"
+    FILENAME_SUFFIX_DIFF = "-diff"
+    FILENAME_SUFFIX_STDERR = "-stderr"
+    FILENAME_SUFFIX_CRASH_LOG = "-crash-log"
+    FILENAME_SUFFIX_WDIFF = "-wdiff.html"
+    FILENAME_SUFFIX_PRETTY_PATCH = "-pretty-diff.html"
+    FILENAME_SUFFIX_IMAGE_DIFF = "-diff.png"
+    FILENAME_SUFFIX_IMAGE_DIFFS_HTML = "-diffs.html"
+
+    def __init__(self, filesystem, port, root_output_dir, test_name):
+        self._filesystem = filesystem
+        self._port = port
+        self._root_output_dir = root_output_dir
+        self._test_name = test_name
+
+    def _make_output_directory(self):
+        """Creates the output directory (if needed) for a given test filename."""
+        fs = self._filesystem
+        output_filename = fs.join(self._root_output_dir, self._test_name)
+        fs.maybe_make_directory(fs.dirname(output_filename))
+
+    def output_filename(self, modifier):
+        """Returns a filename inside the output dir that contains modifier.
+
+        For example, if test name is "fast/dom/foo.html" and modifier is "-expected.txt",
+        the return value is "/<path-to-root-output-dir>/fast/dom/foo-expected.txt".
+
+        Args:
+          modifier: a string to replace the extension of filename with
+
+        Return:
+          The absolute path to the output filename
+        """
+        fs = self._filesystem
+        output_filename = fs.join(self._root_output_dir, self._test_name)
+        return fs.splitext(output_filename)[0] + modifier
+
+    def _write_binary_file(self, path, contents):
+        if contents is not None:
+            self._make_output_directory()
+            self._filesystem.write_binary_file(path, contents)
+
+    def _write_text_file(self, path, contents):
+        if contents is not None:
+            self._make_output_directory()
+            self._filesystem.write_text_file(path, contents)
+
+    def _output_testname(self, modifier):
+        fs = self._filesystem
+        return fs.splitext(fs.basename(self._test_name))[0] + modifier
+
+    def write_output_files(self, file_type, output, expected):
+        """Writes the test output, the expected output in the results directory.
+
+        The full output filename of the actual, for example, will be
+          <filename>-actual<file_type>
+        For instance,
+          my_test-actual.txt
+
+        Args:
+          file_type: A string describing the test output file type, e.g. ".txt"
+          output: A string containing the test output
+          expected: A string containing the expected test output
+        """
+        actual_filename = self.output_filename(self.FILENAME_SUFFIX_ACTUAL + file_type)
+        expected_filename = self.output_filename(self.FILENAME_SUFFIX_EXPECTED + file_type)
+
+        self._write_binary_file(actual_filename, output)
+        self._write_binary_file(expected_filename, expected)
+
+    def write_stderr(self, error):
+        filename = self.output_filename(self.FILENAME_SUFFIX_STDERR + ".txt")
+        self._write_binary_file(filename, error)
+
+    def write_crash_log(self, crash_log):
+        filename = self.output_filename(self.FILENAME_SUFFIX_CRASH_LOG + ".txt")
+        self._write_text_file(filename, crash_log)
+
+    def write_text_files(self, actual_text, expected_text):
+        self.write_output_files(".txt", actual_text, expected_text)
+
+    def create_text_diff_and_write_result(self, actual_text, expected_text):
+        # FIXME: This function is actually doing the diffs as well as writing results.
+        # It might be better to extract code which does 'diff' and make it a separate function.
+        if not actual_text or not expected_text:
+            return
+
+        file_type = '.txt'
+        actual_filename = self.output_filename(self.FILENAME_SUFFIX_ACTUAL + file_type)
+        expected_filename = self.output_filename(self.FILENAME_SUFFIX_EXPECTED + file_type)
+        # We treat diff output as binary. Diff output may contain multiple files
+        # in conflicting encodings.
+        diff = self._port.diff_text(expected_text, actual_text, expected_filename, actual_filename)
+        diff_filename = self.output_filename(self.FILENAME_SUFFIX_DIFF + file_type)
+        self._write_binary_file(diff_filename, diff)
+
+        # Shell out to wdiff to get colored inline diffs.
+        if self._port.wdiff_available():
+            wdiff = self._port.wdiff_text(expected_filename, actual_filename)
+            wdiff_filename = self.output_filename(self.FILENAME_SUFFIX_WDIFF)
+            self._write_binary_file(wdiff_filename, wdiff)
+
+        # Use WebKit's PrettyPatch.rb to get an HTML diff.
+        if self._port.pretty_patch_available():
+            pretty_patch = self._port.pretty_patch_text(diff_filename)
+            pretty_patch_filename = self.output_filename(self.FILENAME_SUFFIX_PRETTY_PATCH)
+            self._write_binary_file(pretty_patch_filename, pretty_patch)
+
+    def write_audio_files(self, actual_audio, expected_audio):
+        self.write_output_files('.wav', actual_audio, expected_audio)
+
+    def write_image_files(self, actual_image, expected_image):
+        self.write_output_files('.png', actual_image, expected_image)
+
+    def write_image_diff_files(self, image_diff):
+        diff_filename = self.output_filename(self.FILENAME_SUFFIX_IMAGE_DIFF)
+        self._write_binary_file(diff_filename, image_diff)
+
+        diffs_html_filename = self.output_filename(self.FILENAME_SUFFIX_IMAGE_DIFFS_HTML)
+        # FIXME: old-run-webkit-tests shows the diff percentage as the text contents of the "diff" link.
+        # FIXME: old-run-webkit-tests include a link to the test file.
+        html = """<!DOCTYPE HTML>
+<html>
+<head>
+<title>%(title)s</title>
+<style>.label{font-weight:bold}</style>
+</head>
+<body>
+Difference between images: <a href="%(diff_filename)s">diff</a><br>
+<div class=imageText></div>
+<div class=imageContainer data-prefix="%(prefix)s">Loading...</div>
+<script>
+(function() {
+    var preloadedImageCount = 0;
+    function preloadComplete() {
+        ++preloadedImageCount;
+        if (preloadedImageCount < 2)
+            return;
+        toggleImages();
+        setInterval(toggleImages, 2000)
+    }
+
+    function preloadImage(url) {
+        image = new Image();
+        image.addEventListener('load', preloadComplete);
+        image.src = url;
+        return image;
+    }
+
+    function toggleImages() {
+        if (text.textContent == 'Expected Image') {
+            text.textContent = 'Actual Image';
+            container.replaceChild(actualImage, container.firstChild);
+        } else {
+            text.textContent = 'Expected Image';
+            container.replaceChild(expectedImage, container.firstChild);
+        }
+    }
+
+    var text = document.querySelector('.imageText');
+    var container = document.querySelector('.imageContainer');
+    var actualImage = preloadImage(container.getAttribute('data-prefix') + '-actual.png');
+    var expectedImage = preloadImage(container.getAttribute('data-prefix') + '-expected.png');
+})();
+</script>
+</body>
+</html>
+""" % {
+            'title': self._test_name,
+            'diff_filename': self._output_testname(self.FILENAME_SUFFIX_IMAGE_DIFF),
+            'prefix': self._output_testname(''),
+        }
+        self._filesystem.write_text_file(diffs_html_filename, html)
+
+    def write_reftest(self, src_filepath):
+        fs = self._filesystem
+        dst_dir = fs.dirname(fs.join(self._root_output_dir, self._test_name))
+        dst_filepath = fs.join(dst_dir, fs.basename(src_filepath))
+        self._write_text_file(dst_filepath, fs.read_text_file(src_filepath))
diff --git a/Tools/Scripts/webkitpy/layout_tests/controllers/test_result_writer_unittest.py b/Tools/Scripts/webkitpy/layout_tests/controllers/test_result_writer_unittest.py
new file mode 100644
index 0000000..dfd6041
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/controllers/test_result_writer_unittest.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.common.host_mock import MockHost
+from webkitpy.layout_tests.controllers import test_result_writer
+from webkitpy.layout_tests.models import test_failures
+from webkitpy.layout_tests.port.driver import DriverOutput
+from webkitpy.layout_tests.port.test import TestPort
+
+
+class TestResultWriterTest(unittest.TestCase):
+
+    def test_reftest_diff_image(self):
+        """A write_test_result should call port.diff_image with tolerance=0 in case of FailureReftestMismatch."""
+        used_tolerance_values = []
+
+        class ImageDiffTestPort(TestPort):
+            def diff_image(self, expected_contents, actual_contents, tolerance=None):
+                used_tolerance_values.append(tolerance)
+                return (True, 1, None)
+
+        host = MockHost()
+        port = ImageDiffTestPort(host)
+        test_name = 'failures/unexpected/reftest.html'
+        test_reference_file = host.filesystem.join(port.layout_tests_dir(), 'failures/unexpected/reftest-expected.html')
+        driver_output1 = DriverOutput('text1', 'image1', 'imagehash1', 'audio1')
+        driver_output2 = DriverOutput('text2', 'image2', 'imagehash2', 'audio2')
+        failures = [test_failures.FailureReftestMismatch(test_reference_file)]
+        test_result_writer.write_test_result(host.filesystem, ImageDiffTestPort(host), test_name,
+                                             driver_output1, driver_output2, failures)
+        self.assertEqual([0], used_tolerance_values)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/layout_tests/layout_package/__init__.py b/Tools/Scripts/webkitpy/layout_tests/layout_package/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/layout_package/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py b/Tools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py
new file mode 100644
index 0000000..f277c93
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py
@@ -0,0 +1,176 @@
+# 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 logging
+
+from webkitpy.layout_tests.layout_package import json_results_generator
+from webkitpy.layout_tests.models import test_expectations
+from webkitpy.layout_tests.models import test_failures
+
+class JSONLayoutResultsGenerator(json_results_generator.JSONResultsGeneratorBase):
+    """A JSON results generator for layout tests."""
+
+    LAYOUT_TESTS_PATH = "LayoutTests"
+
+    # Additional JSON fields.
+    WONTFIX = "wontfixCounts"
+
+    FAILURE_TO_CHAR = {test_expectations.PASS: json_results_generator.JSONResultsGeneratorBase.PASS_RESULT,
+                       test_expectations.SKIP: json_results_generator.JSONResultsGeneratorBase.SKIP_RESULT,
+                       test_expectations.CRASH: "C",
+                       test_expectations.TIMEOUT: "T",
+                       test_expectations.IMAGE: "I",
+                       test_expectations.TEXT: "F",
+                       test_expectations.AUDIO: "A",
+                       test_expectations.MISSING: "O",
+                       test_expectations.IMAGE_PLUS_TEXT: "Z"}
+
+    def __init__(self, port, builder_name, build_name, build_number,
+        results_file_base_path, builder_base_url,
+        test_timings, expectations, result_summary, all_tests,
+        test_results_server=None, test_type="", master_name=""):
+        """Modifies the results.json file. Grabs it off the archive directory
+        if it is not found locally.
+
+        Args:
+          result_summary: ResultsSummary object storing the summary of the test
+              results.
+        """
+        super(JSONLayoutResultsGenerator, self).__init__(
+            port, builder_name, build_name, build_number, results_file_base_path,
+            builder_base_url, {}, port.repository_paths(),
+            test_results_server, test_type, master_name)
+
+        self._expectations = expectations
+
+        self._result_summary = result_summary
+        self._failures = dict((test_name, result_summary.results[test_name].type) for test_name in result_summary.failures)
+        self._all_tests = all_tests
+        self._test_timings = dict((test_tuple.test_name, test_tuple.test_run_time) for test_tuple in test_timings)
+
+        self.generate_json_output()
+
+    def _get_path_relative_to_layout_test_root(self, test):
+        """Returns the path of the test relative to the layout test root.
+        For example, for:
+          src/third_party/WebKit/LayoutTests/fast/forms/foo.html
+        We would return
+          fast/forms/foo.html
+        """
+        index = test.find(self.LAYOUT_TESTS_PATH)
+        if index is not -1:
+            index += len(self.LAYOUT_TESTS_PATH)
+
+        if index is -1:
+            # Already a relative path.
+            relativePath = test
+        else:
+            relativePath = test[index + 1:]
+
+        # Make sure all paths are unix-style.
+        return relativePath.replace('\\', '/')
+
+    # override
+    def _get_test_timing(self, test_name):
+        if test_name in self._test_timings:
+            # Floor for now to get time in seconds.
+            return int(self._test_timings[test_name])
+        return 0
+
+    # override
+    def _get_failed_test_names(self):
+        return set(self._failures.keys())
+
+    # override
+    def _get_modifier_char(self, test_name):
+        if test_name not in self._all_tests:
+            return self.NO_DATA_RESULT
+
+        if test_name in self._failures:
+            return self.FAILURE_TO_CHAR[self._failures[test_name]]
+
+        return self.PASS_RESULT
+
+    # override
+    def _get_result_char(self, test_name):
+        return self._get_modifier_char(test_name)
+
+    # override
+    def _insert_failure_summaries(self, results_for_builder):
+        summary = self._result_summary
+
+        self._insert_item_into_raw_list(results_for_builder,
+            len((set(summary.failures.keys()) |
+                summary.tests_by_expectation[test_expectations.SKIP]) &
+                summary.tests_by_timeline[test_expectations.NOW]),
+            self.FIXABLE_COUNT)
+        self._insert_item_into_raw_list(results_for_builder,
+            self._get_failure_summary_entry(test_expectations.NOW),
+            self.FIXABLE)
+        self._insert_item_into_raw_list(results_for_builder,
+            len(self._expectations.get_tests_with_timeline(
+                test_expectations.NOW)), self.ALL_FIXABLE_COUNT)
+        self._insert_item_into_raw_list(results_for_builder,
+            self._get_failure_summary_entry(test_expectations.WONTFIX),
+            self.WONTFIX)
+
+    # override
+    def _normalize_results_json(self, test, test_name, tests):
+        super(JSONLayoutResultsGenerator, self)._normalize_results_json(
+            test, test_name, tests)
+
+        # Remove tests that don't exist anymore.
+        full_path = self._filesystem.join(self._port.layout_tests_dir(), test_name)
+        full_path = self._filesystem.normpath(full_path)
+        if not self._filesystem.exists(full_path):
+            del tests[test_name]
+
+    def _get_failure_summary_entry(self, timeline):
+        """Creates a summary object to insert into the JSON.
+
+        Args:
+          summary   ResultSummary object with test results
+          timeline  current test_expectations timeline to build entry for
+                    (e.g., test_expectations.NOW, etc.)
+        """
+        entry = {}
+        summary = self._result_summary
+        timeline_tests = summary.tests_by_timeline[timeline]
+        entry[self.SKIP_RESULT] = len(
+            summary.tests_by_expectation[test_expectations.SKIP] &
+            timeline_tests)
+        entry[self.PASS_RESULT] = len(
+            summary.tests_by_expectation[test_expectations.PASS] &
+            timeline_tests)
+        for failure_type in summary.tests_by_expectation.keys():
+            if failure_type not in self.FAILURE_TO_CHAR:
+                continue
+            count = len(summary.tests_by_expectation[failure_type] &
+                        timeline_tests)
+            entry[self.FAILURE_TO_CHAR[failure_type]] = count
+        return entry
diff --git a/Tools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py b/Tools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py
new file mode 100644
index 0000000..73834f0
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py
@@ -0,0 +1,660 @@
+# 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 json
+import logging
+import subprocess
+import sys
+import time
+import urllib2
+import xml.dom.minidom
+
+from webkitpy.common.checkout.scm.detection import SCMDetector
+from webkitpy.common.net.file_uploader import FileUploader
+
+# A JSON results generator for generic tests.
+# FIXME: move this code out of the layout_package directory.
+
+_log = logging.getLogger(__name__)
+
+_JSON_PREFIX = "ADD_RESULTS("
+_JSON_SUFFIX = ");"
+
+
+def has_json_wrapper(string):
+    return string.startswith(_JSON_PREFIX) and string.endswith(_JSON_SUFFIX)
+
+
+def strip_json_wrapper(json_content):
+    # FIXME: Kill this code once the server returns json instead of jsonp.
+    if has_json_wrapper(json_content):
+        return json_content[len(_JSON_PREFIX):len(json_content) - len(_JSON_SUFFIX)]
+    return json_content
+
+
+def load_json(filesystem, file_path):
+    content = filesystem.read_text_file(file_path)
+    content = strip_json_wrapper(content)
+    return json.loads(content)
+
+
+def write_json(filesystem, json_object, file_path, callback=None):
+    # Specify separators in order to get compact encoding.
+    json_string = json.dumps(json_object, separators=(',', ':'))
+    if callback:
+        json_string = callback + "(" + json_string + ");"
+    filesystem.write_text_file(file_path, json_string)
+
+
+def convert_trie_to_flat_paths(trie, prefix=None):
+    """Converts the directory structure in the given trie to flat paths, prepending a prefix to each."""
+    result = {}
+    for name, data in trie.iteritems():
+        if prefix:
+            name = prefix + "/" + name
+
+        if len(data) and not "results" in data:
+            result.update(convert_trie_to_flat_paths(data, name))
+        else:
+            result[name] = data
+
+    return result
+
+
+def add_path_to_trie(path, value, trie):
+    """Inserts a single flat directory path and associated value into a directory trie structure."""
+    if not "/" in path:
+        trie[path] = value
+        return
+
+    directory, slash, rest = path.partition("/")
+    if not directory in trie:
+        trie[directory] = {}
+    add_path_to_trie(rest, value, trie[directory])
+
+def test_timings_trie(port, individual_test_timings):
+    """Breaks a test name into chunks by directory and puts the test time as a value in the lowest part, e.g.
+    foo/bar/baz.html: 1ms
+    foo/bar/baz1.html: 3ms
+
+    becomes
+    foo: {
+        bar: {
+            baz.html: 1,
+            baz1.html: 3
+        }
+    }
+    """
+    trie = {}
+    for test_result in individual_test_timings:
+        test = test_result.test_name
+
+        add_path_to_trie(test, int(1000 * test_result.test_run_time), trie)
+
+    return trie
+
+# FIXME: We already have a TestResult class in test_results.py
+class TestResult(object):
+    """A simple class that represents a single test result."""
+
+    # Test modifier constants.
+    (NONE, FAILS, FLAKY, DISABLED) = range(4)
+
+    def __init__(self, test, failed=False, elapsed_time=0):
+        self.test_name = test
+        self.failed = failed
+        self.test_run_time = elapsed_time
+
+        test_name = test
+        try:
+            test_name = test.split('.')[1]
+        except IndexError:
+            _log.warn("Invalid test name: %s.", test)
+            pass
+
+        if test_name.startswith('FAILS_'):
+            self.modifier = self.FAILS
+        elif test_name.startswith('FLAKY_'):
+            self.modifier = self.FLAKY
+        elif test_name.startswith('DISABLED_'):
+            self.modifier = self.DISABLED
+        else:
+            self.modifier = self.NONE
+
+    def fixable(self):
+        return self.failed or self.modifier == self.DISABLED
+
+
+class JSONResultsGeneratorBase(object):
+    """A JSON results generator for generic tests."""
+
+    MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG = 750
+    # Min time (seconds) that will be added to the JSON.
+    MIN_TIME = 1
+
+    # Note that in non-chromium tests those chars are used to indicate
+    # test modifiers (FAILS, FLAKY, etc) but not actual test results.
+    PASS_RESULT = "P"
+    SKIP_RESULT = "X"
+    FAIL_RESULT = "F"
+    FLAKY_RESULT = "L"
+    NO_DATA_RESULT = "N"
+
+    MODIFIER_TO_CHAR = {TestResult.NONE: PASS_RESULT,
+                        TestResult.DISABLED: SKIP_RESULT,
+                        TestResult.FAILS: FAIL_RESULT,
+                        TestResult.FLAKY: FLAKY_RESULT}
+
+    VERSION = 4
+    VERSION_KEY = "version"
+    RESULTS = "results"
+    TIMES = "times"
+    BUILD_NUMBERS = "buildNumbers"
+    TIME = "secondsSinceEpoch"
+    TESTS = "tests"
+
+    FIXABLE_COUNT = "fixableCount"
+    FIXABLE = "fixableCounts"
+    ALL_FIXABLE_COUNT = "allFixableCount"
+
+    RESULTS_FILENAME = "results.json"
+    TIMES_MS_FILENAME = "times_ms.json"
+    INCREMENTAL_RESULTS_FILENAME = "incremental_results.json"
+
+    URL_FOR_TEST_LIST_JSON = "http://%s/testfile?builder=%s&name=%s&testlistjson=1&testtype=%s&master=%s"
+
+    # FIXME: Remove generate_incremental_results once the reference to it in
+    # http://src.chromium.org/viewvc/chrome/trunk/tools/build/scripts/slave/gtest_slave_utils.py
+    # has been removed.
+    def __init__(self, port, builder_name, build_name, build_number,
+        results_file_base_path, builder_base_url,
+        test_results_map, svn_repositories=None,
+        test_results_server=None,
+        test_type="",
+        master_name="",
+        generate_incremental_results=None):
+        """Modifies the results.json file. Grabs it off the archive directory
+        if it is not found locally.
+
+        Args
+          port: port-specific wrapper
+          builder_name: the builder name (e.g. Webkit).
+          build_name: the build name (e.g. webkit-rel).
+          build_number: the build number.
+          results_file_base_path: Absolute path to the directory containing the
+              results json file.
+          builder_base_url: the URL where we have the archived test results.
+              If this is None no archived results will be retrieved.
+          test_results_map: A dictionary that maps test_name to TestResult.
+          svn_repositories: A (json_field_name, svn_path) pair for SVN
+              repositories that tests rely on.  The SVN revision will be
+              included in the JSON with the given json_field_name.
+          test_results_server: server that hosts test results json.
+          test_type: test type string (e.g. 'layout-tests').
+          master_name: the name of the buildbot master.
+        """
+        self._port = port
+        self._filesystem = port._filesystem
+        self._executive = port._executive
+        self._builder_name = builder_name
+        self._build_name = build_name
+        self._build_number = build_number
+        self._builder_base_url = builder_base_url
+        self._results_directory = results_file_base_path
+
+        self._test_results_map = test_results_map
+        self._test_results = test_results_map.values()
+
+        self._svn_repositories = svn_repositories
+        if not self._svn_repositories:
+            self._svn_repositories = {}
+
+        self._test_results_server = test_results_server
+        self._test_type = test_type
+        self._master_name = master_name
+
+        self._archived_results = None
+
+    def generate_json_output(self):
+        json_object = self.get_json()
+        if json_object:
+            file_path = self._filesystem.join(self._results_directory, self.INCREMENTAL_RESULTS_FILENAME)
+            write_json(self._filesystem, json_object, file_path)
+
+    def generate_times_ms_file(self):
+        # FIXME: rename to generate_times_ms_file. This needs to be coordinated with
+        # changing the calls to this on the chromium build slaves.
+        times = test_timings_trie(self._port, self._test_results_map.values())
+        file_path = self._filesystem.join(self._results_directory, self.TIMES_MS_FILENAME)
+        write_json(self._filesystem, times, file_path)
+
+    def get_json(self):
+        """Gets the results for the results.json file."""
+        results_json = {}
+
+        if not results_json:
+            results_json, error = self._get_archived_json_results()
+            if error:
+                # If there was an error don't write a results.json
+                # file at all as it would lose all the information on the
+                # bot.
+                _log.error("Archive directory is inaccessible. Not "
+                           "modifying or clobbering the results.json "
+                           "file: " + str(error))
+                return None
+
+        builder_name = self._builder_name
+        if results_json and builder_name not in results_json:
+            _log.debug("Builder name (%s) is not in the results.json file."
+                       % builder_name)
+
+        self._convert_json_to_current_version(results_json)
+
+        if builder_name not in results_json:
+            results_json[builder_name] = (
+                self._create_results_for_builder_json())
+
+        results_for_builder = results_json[builder_name]
+
+        if builder_name:
+            self._insert_generic_metadata(results_for_builder)
+
+        self._insert_failure_summaries(results_for_builder)
+
+        # Update the all failing tests with result type and time.
+        tests = results_for_builder[self.TESTS]
+        all_failing_tests = self._get_failed_test_names()
+        all_failing_tests.update(convert_trie_to_flat_paths(tests))
+
+        for test in all_failing_tests:
+            self._insert_test_time_and_result(test, tests)
+
+        return results_json
+
+    def set_archived_results(self, archived_results):
+        self._archived_results = archived_results
+
+    def upload_json_files(self, json_files):
+        """Uploads the given json_files to the test_results_server (if the
+        test_results_server is given)."""
+        if not self._test_results_server:
+            return
+
+        if not self._master_name:
+            _log.error("--test-results-server was set, but --master-name was not.  Not uploading JSON files.")
+            return
+
+        _log.info("Uploading JSON files for builder: %s", self._builder_name)
+        attrs = [("builder", self._builder_name),
+                 ("testtype", self._test_type),
+                 ("master", self._master_name)]
+
+        files = [(file, self._filesystem.join(self._results_directory, file))
+            for file in json_files]
+
+        url = "http://%s/testfile/upload" % self._test_results_server
+        # Set uploading timeout in case appengine server is having problems.
+        # 120 seconds are more than enough to upload test results.
+        uploader = FileUploader(url, 120)
+        try:
+            response = uploader.upload_as_multipart_form_data(self._filesystem, files, attrs)
+            if response:
+                if response.code == 200:
+                    _log.info("JSON uploaded.")
+                else:
+                    _log.debug("JSON upload failed, %d: '%s'" % (response.code, response.read()))
+            else:
+                _log.error("JSON upload failed; no response returned")
+        except Exception, err:
+            _log.error("Upload failed: %s" % err)
+            return
+
+
+    def _get_test_timing(self, test_name):
+        """Returns test timing data (elapsed time) in second
+        for the given test_name."""
+        if test_name in self._test_results_map:
+            # Floor for now to get time in seconds.
+            return int(self._test_results_map[test_name].test_run_time)
+        return 0
+
+    def _get_failed_test_names(self):
+        """Returns a set of failed test names."""
+        return set([r.test_name for r in self._test_results if r.failed])
+
+    def _get_modifier_char(self, test_name):
+        """Returns a single char (e.g. SKIP_RESULT, FAIL_RESULT,
+        PASS_RESULT, NO_DATA_RESULT, etc) that indicates the test modifier
+        for the given test_name.
+        """
+        if test_name not in self._test_results_map:
+            return self.__class__.NO_DATA_RESULT
+
+        test_result = self._test_results_map[test_name]
+        if test_result.modifier in self.MODIFIER_TO_CHAR.keys():
+            return self.MODIFIER_TO_CHAR[test_result.modifier]
+
+        return self.__class__.PASS_RESULT
+
+    def _get_result_char(self, test_name):
+        """Returns a single char (e.g. SKIP_RESULT, FAIL_RESULT,
+        PASS_RESULT, NO_DATA_RESULT, etc) that indicates the test result
+        for the given test_name.
+        """
+        if test_name not in self._test_results_map:
+            return self.__class__.NO_DATA_RESULT
+
+        test_result = self._test_results_map[test_name]
+        if test_result.modifier == TestResult.DISABLED:
+            return self.__class__.SKIP_RESULT
+
+        if test_result.failed:
+            return self.__class__.FAIL_RESULT
+
+        return self.__class__.PASS_RESULT
+
+    def _get_svn_revision(self, in_directory):
+        """Returns the svn revision for the given directory.
+
+        Args:
+          in_directory: The directory where svn is to be run.
+        """
+
+        # FIXME: We initialize this here in order to engage the stupid windows hacks :).
+        # We can't reuse an existing scm object because the specific directories may
+        # be part of other checkouts.
+        self._port.host.initialize_scm()
+        scm = SCMDetector(self._filesystem, self._executive).detect_scm_system(in_directory)
+        if scm:
+            return scm.svn_revision(in_directory)
+        return ""
+
+    def _get_archived_json_results(self):
+        """Download JSON file that only contains test
+        name list from test-results server. This is for generating incremental
+        JSON so the file generated has info for tests that failed before but
+        pass or are skipped from current run.
+
+        Returns (archived_results, error) tuple where error is None if results
+        were successfully read.
+        """
+        results_json = {}
+        old_results = None
+        error = None
+
+        if not self._test_results_server:
+            return {}, None
+
+        results_file_url = (self.URL_FOR_TEST_LIST_JSON %
+            (urllib2.quote(self._test_results_server),
+             urllib2.quote(self._builder_name),
+             self.RESULTS_FILENAME,
+             urllib2.quote(self._test_type),
+             urllib2.quote(self._master_name)))
+
+        try:
+            # FIXME: We should talk to the network via a Host object.
+            results_file = urllib2.urlopen(results_file_url)
+            info = results_file.info()
+            old_results = results_file.read()
+        except urllib2.HTTPError, http_error:
+            # A non-4xx status code means the bot is hosed for some reason
+            # and we can't grab the results.json file off of it.
+            if (http_error.code < 400 and http_error.code >= 500):
+                error = http_error
+        except urllib2.URLError, url_error:
+            error = url_error
+
+        if old_results:
+            # Strip the prefix and suffix so we can get the actual JSON object.
+            old_results = strip_json_wrapper(old_results)
+
+            try:
+                results_json = json.loads(old_results)
+            except:
+                _log.debug("results.json was not valid JSON. Clobbering.")
+                # The JSON file is not valid JSON. Just clobber the results.
+                results_json = {}
+        else:
+            _log.debug('Old JSON results do not exist. Starting fresh.')
+            results_json = {}
+
+        return results_json, error
+
+    def _insert_failure_summaries(self, results_for_builder):
+        """Inserts aggregate pass/failure statistics into the JSON.
+        This method reads self._test_results and generates
+        FIXABLE, FIXABLE_COUNT and ALL_FIXABLE_COUNT entries.
+
+        Args:
+          results_for_builder: Dictionary containing the test results for a
+              single builder.
+        """
+        # Insert the number of tests that failed or skipped.
+        fixable_count = len([r for r in self._test_results if r.fixable()])
+        self._insert_item_into_raw_list(results_for_builder,
+            fixable_count, self.FIXABLE_COUNT)
+
+        # Create a test modifiers (FAILS, FLAKY etc) summary dictionary.
+        entry = {}
+        for test_name in self._test_results_map.iterkeys():
+            result_char = self._get_modifier_char(test_name)
+            entry[result_char] = entry.get(result_char, 0) + 1
+
+        # Insert the pass/skip/failure summary dictionary.
+        self._insert_item_into_raw_list(results_for_builder, entry,
+                                        self.FIXABLE)
+
+        # Insert the number of all the tests that are supposed to pass.
+        all_test_count = len(self._test_results)
+        self._insert_item_into_raw_list(results_for_builder,
+            all_test_count, self.ALL_FIXABLE_COUNT)
+
+    def _insert_item_into_raw_list(self, results_for_builder, item, key):
+        """Inserts the item into the list with the given key in the results for
+        this builder. Creates the list if no such list exists.
+
+        Args:
+          results_for_builder: Dictionary containing the test results for a
+              single builder.
+          item: Number or string to insert into the list.
+          key: Key in results_for_builder for the list to insert into.
+        """
+        if key in results_for_builder:
+            raw_list = results_for_builder[key]
+        else:
+            raw_list = []
+
+        raw_list.insert(0, item)
+        raw_list = raw_list[:self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG]
+        results_for_builder[key] = raw_list
+
+    def _insert_item_run_length_encoded(self, item, encoded_results):
+        """Inserts the item into the run-length encoded results.
+
+        Args:
+          item: String or number to insert.
+          encoded_results: run-length encoded results. An array of arrays, e.g.
+              [[3,'A'],[1,'Q']] encodes AAAQ.
+        """
+        if len(encoded_results) and item == encoded_results[0][1]:
+            num_results = encoded_results[0][0]
+            if num_results <= self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG:
+                encoded_results[0][0] = num_results + 1
+        else:
+            # Use a list instead of a class for the run-length encoding since
+            # we want the serialized form to be concise.
+            encoded_results.insert(0, [1, item])
+
+    def _insert_generic_metadata(self, results_for_builder):
+        """ Inserts generic metadata (such as version number, current time etc)
+        into the JSON.
+
+        Args:
+          results_for_builder: Dictionary containing the test results for
+              a single builder.
+        """
+        self._insert_item_into_raw_list(results_for_builder,
+            self._build_number, self.BUILD_NUMBERS)
+
+        # Include SVN revisions for the given repositories.
+        for (name, path) in self._svn_repositories:
+            # Note: for JSON file's backward-compatibility we use 'chrome' rather
+            # than 'chromium' here.
+            if name == 'chromium':
+                name = 'chrome'
+            self._insert_item_into_raw_list(results_for_builder,
+                self._get_svn_revision(path),
+                name + 'Revision')
+
+        self._insert_item_into_raw_list(results_for_builder,
+            int(time.time()),
+            self.TIME)
+
+    def _insert_test_time_and_result(self, test_name, tests):
+        """ Insert a test item with its results to the given tests dictionary.
+
+        Args:
+          tests: Dictionary containing test result entries.
+        """
+
+        result = self._get_result_char(test_name)
+        time = self._get_test_timing(test_name)
+
+        this_test = tests
+        for segment in test_name.split("/"):
+            if segment not in this_test:
+                this_test[segment] = {}
+            this_test = this_test[segment]
+
+        if not len(this_test):
+            self._populate_results_and_times_json(this_test)
+
+        if self.RESULTS in this_test:
+            self._insert_item_run_length_encoded(result, this_test[self.RESULTS])
+        else:
+            this_test[self.RESULTS] = [[1, result]]
+
+        if self.TIMES in this_test:
+            self._insert_item_run_length_encoded(time, this_test[self.TIMES])
+        else:
+            this_test[self.TIMES] = [[1, time]]
+
+    def _convert_json_to_current_version(self, results_json):
+        """If the JSON does not match the current version, converts it to the
+        current version and adds in the new version number.
+        """
+        if self.VERSION_KEY in results_json:
+            archive_version = results_json[self.VERSION_KEY]
+            if archive_version == self.VERSION:
+                return
+        else:
+            archive_version = 3
+
+        # version 3->4
+        if archive_version == 3:
+            num_results = len(results_json.values())
+            for builder, results in results_json.iteritems():
+                self._convert_tests_to_trie(results)
+
+        results_json[self.VERSION_KEY] = self.VERSION
+
+    def _convert_tests_to_trie(self, results):
+        if not self.TESTS in results:
+            return
+
+        test_results = results[self.TESTS]
+        test_results_trie = {}
+        for test in test_results.iterkeys():
+            single_test_result = test_results[test]
+            add_path_to_trie(test, single_test_result, test_results_trie)
+
+        results[self.TESTS] = test_results_trie
+
+    def _populate_results_and_times_json(self, results_and_times):
+        results_and_times[self.RESULTS] = []
+        results_and_times[self.TIMES] = []
+        return results_and_times
+
+    def _create_results_for_builder_json(self):
+        results_for_builder = {}
+        results_for_builder[self.TESTS] = {}
+        return results_for_builder
+
+    def _remove_items_over_max_number_of_builds(self, encoded_list):
+        """Removes items from the run-length encoded list after the final
+        item that exceeds the max number of builds to track.
+
+        Args:
+          encoded_results: run-length encoded results. An array of arrays, e.g.
+              [[3,'A'],[1,'Q']] encodes AAAQ.
+        """
+        num_builds = 0
+        index = 0
+        for result in encoded_list:
+            num_builds = num_builds + result[0]
+            index = index + 1
+            if num_builds > self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG:
+                return encoded_list[:index]
+        return encoded_list
+
+    def _normalize_results_json(self, test, test_name, tests):
+        """ Prune tests where all runs pass or tests that no longer exist and
+        truncate all results to maxNumberOfBuilds.
+
+        Args:
+          test: ResultsAndTimes object for this test.
+          test_name: Name of the test.
+          tests: The JSON object with all the test results for this builder.
+        """
+        test[self.RESULTS] = self._remove_items_over_max_number_of_builds(
+            test[self.RESULTS])
+        test[self.TIMES] = self._remove_items_over_max_number_of_builds(
+            test[self.TIMES])
+
+        is_all_pass = self._is_results_all_of_type(test[self.RESULTS],
+                                                   self.PASS_RESULT)
+        is_all_no_data = self._is_results_all_of_type(test[self.RESULTS],
+            self.NO_DATA_RESULT)
+        max_time = max([time[1] for time in test[self.TIMES]])
+
+        # Remove all passes/no-data from the results to reduce noise and
+        # filesize. If a test passes every run, but takes > MIN_TIME to run,
+        # don't throw away the data.
+        if is_all_no_data or (is_all_pass and max_time <= self.MIN_TIME):
+            del tests[test_name]
+
+    def _is_results_all_of_type(self, results, type):
+        """Returns whether all the results are of the given type
+        (e.g. all passes)."""
+        return len(results) == 1 and results[0][1] == type
+
+
+# Left here not to break anything.
+class JSONResultsGenerator(JSONResultsGeneratorBase):
+    pass
diff --git a/Tools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator_unittest.py b/Tools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator_unittest.py
new file mode 100644
index 0000000..f04300f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator_unittest.py
@@ -0,0 +1,235 @@
+# 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 json
+import optparse
+import random
+
+from webkitpy.common.host_mock import MockHost
+from webkitpy.layout_tests.layout_package import json_results_generator
+from webkitpy.layout_tests.models import test_expectations
+from webkitpy.layout_tests.port import test
+from webkitpy.thirdparty.mock import Mock
+
+
+class JSONGeneratorTest(unittest.TestCase):
+    def setUp(self):
+        self.builder_name = 'DUMMY_BUILDER_NAME'
+        self.build_name = 'DUMMY_BUILD_NAME'
+        self.build_number = 'DUMMY_BUILDER_NUMBER'
+
+        # For archived results.
+        self._json = None
+        self._num_runs = 0
+        self._tests_set = set([])
+        self._test_timings = {}
+        self._failed_count_map = {}
+
+        self._PASS_count = 0
+        self._DISABLED_count = 0
+        self._FLAKY_count = 0
+        self._FAILS_count = 0
+        self._fixable_count = 0
+
+    def test_strip_json_wrapper(self):
+        json = "['contents']"
+        self.assertEqual(json_results_generator.strip_json_wrapper(json_results_generator._JSON_PREFIX + json + json_results_generator._JSON_SUFFIX), json)
+        self.assertEqual(json_results_generator.strip_json_wrapper(json), json)
+
+    def _test_json_generation(self, passed_tests_list, failed_tests_list):
+        tests_set = set(passed_tests_list) | set(failed_tests_list)
+
+        DISABLED_tests = set([t for t in tests_set
+                             if t.startswith('DISABLED_')])
+        FLAKY_tests = set([t for t in tests_set
+                           if t.startswith('FLAKY_')])
+        FAILS_tests = set([t for t in tests_set
+                           if t.startswith('FAILS_')])
+        PASS_tests = tests_set - (DISABLED_tests | FLAKY_tests | FAILS_tests)
+
+        failed_tests = set(failed_tests_list) - DISABLED_tests
+        failed_count_map = dict([(t, 1) for t in failed_tests])
+
+        test_timings = {}
+        i = 0
+        for test in tests_set:
+            test_timings[test] = float(self._num_runs * 100 + i)
+            i += 1
+
+        test_results_map = dict()
+        for test in tests_set:
+            test_results_map[test] = json_results_generator.TestResult(test,
+                failed=(test in failed_tests),
+                elapsed_time=test_timings[test])
+
+        host = MockHost()
+        port = Mock()
+        port._filesystem = host.filesystem
+        generator = json_results_generator.JSONResultsGeneratorBase(port,
+            self.builder_name, self.build_name, self.build_number,
+            '',
+            None,   # don't fetch past json results archive
+            test_results_map)
+
+        failed_count_map = dict([(t, 1) for t in failed_tests])
+
+        # Test incremental json results
+        incremental_json = generator.get_json()
+        self._verify_json_results(
+            tests_set,
+            test_timings,
+            failed_count_map,
+            len(PASS_tests),
+            len(DISABLED_tests),
+            len(FLAKY_tests),
+            len(DISABLED_tests | failed_tests),
+            incremental_json,
+            1)
+
+        # We don't verify the results here, but at least we make sure the code runs without errors.
+        generator.generate_json_output()
+        generator.generate_times_ms_file()
+
+    def _verify_json_results(self, tests_set, test_timings, failed_count_map,
+                             PASS_count, DISABLED_count, FLAKY_count,
+                             fixable_count,
+                             json, num_runs):
+        # Aliasing to a short name for better access to its constants.
+        JRG = json_results_generator.JSONResultsGeneratorBase
+
+        self.assertTrue(JRG.VERSION_KEY in json)
+        self.assertTrue(self.builder_name in json)
+
+        buildinfo = json[self.builder_name]
+        self.assertTrue(JRG.FIXABLE in buildinfo)
+        self.assertTrue(JRG.TESTS in buildinfo)
+        self.assertEqual(len(buildinfo[JRG.BUILD_NUMBERS]), num_runs)
+        self.assertEqual(buildinfo[JRG.BUILD_NUMBERS][0], self.build_number)
+
+        if tests_set or DISABLED_count:
+            fixable = {}
+            for fixable_items in buildinfo[JRG.FIXABLE]:
+                for (type, count) in fixable_items.iteritems():
+                    if type in fixable:
+                        fixable[type] = fixable[type] + count
+                    else:
+                        fixable[type] = count
+
+            if PASS_count:
+                self.assertEqual(fixable[JRG.PASS_RESULT], PASS_count)
+            else:
+                self.assertTrue(JRG.PASS_RESULT not in fixable or
+                                fixable[JRG.PASS_RESULT] == 0)
+            if DISABLED_count:
+                self.assertEqual(fixable[JRG.SKIP_RESULT], DISABLED_count)
+            else:
+                self.assertTrue(JRG.SKIP_RESULT not in fixable or
+                                fixable[JRG.SKIP_RESULT] == 0)
+            if FLAKY_count:
+                self.assertEqual(fixable[JRG.FLAKY_RESULT], FLAKY_count)
+            else:
+                self.assertTrue(JRG.FLAKY_RESULT not in fixable or
+                                fixable[JRG.FLAKY_RESULT] == 0)
+
+        if failed_count_map:
+            tests = buildinfo[JRG.TESTS]
+            for test_name in failed_count_map.iterkeys():
+                test = self._find_test_in_trie(test_name, tests)
+
+                failed = 0
+                for result in test[JRG.RESULTS]:
+                    if result[1] == JRG.FAIL_RESULT:
+                        failed += result[0]
+                self.assertEqual(failed_count_map[test_name], failed)
+
+                timing_count = 0
+                for timings in test[JRG.TIMES]:
+                    if timings[1] == test_timings[test_name]:
+                        timing_count = timings[0]
+                self.assertEqual(1, timing_count)
+
+        if fixable_count:
+            self.assertEqual(sum(buildinfo[JRG.FIXABLE_COUNT]), fixable_count)
+
+    def _find_test_in_trie(self, path, trie):
+        nodes = path.split("/")
+        sub_trie = trie
+        for node in nodes:
+            self.assertTrue(node in sub_trie)
+            sub_trie = sub_trie[node]
+        return sub_trie
+
+    def test_json_generation(self):
+        self._test_json_generation([], [])
+        self._test_json_generation(['A1', 'B1'], [])
+        self._test_json_generation([], ['FAILS_A2', 'FAILS_B2'])
+        self._test_json_generation(['DISABLED_A3', 'DISABLED_B3'], [])
+        self._test_json_generation(['A4'], ['B4', 'FAILS_C4'])
+        self._test_json_generation(['DISABLED_C5', 'DISABLED_D5'], ['A5', 'B5'])
+        self._test_json_generation(
+            ['A6', 'B6', 'FAILS_C6', 'DISABLED_E6', 'DISABLED_F6'],
+            ['FAILS_D6'])
+
+        # Generate JSON with the same test sets. (Both incremental results and
+        # archived results must be updated appropriately.)
+        self._test_json_generation(
+            ['A', 'FLAKY_B', 'DISABLED_C'],
+            ['FAILS_D', 'FLAKY_E'])
+        self._test_json_generation(
+            ['A', 'DISABLED_C', 'FLAKY_E'],
+            ['FLAKY_B', 'FAILS_D'])
+        self._test_json_generation(
+            ['FLAKY_B', 'DISABLED_C', 'FAILS_D'],
+            ['A', 'FLAKY_E'])
+
+    def test_hierarchical_json_generation(self):
+        # FIXME: Re-work tests to be more comprehensible and comprehensive.
+        self._test_json_generation(['foo/A'], ['foo/B', 'bar/C'])
+
+    def test_test_timings_trie(self):
+        test_port = test.TestPort(MockHost())
+        individual_test_timings = []
+        individual_test_timings.append(json_results_generator.TestResult('foo/bar/baz.html', elapsed_time=1.2))
+        individual_test_timings.append(json_results_generator.TestResult('bar.html', elapsed_time=0.0001))
+        trie = json_results_generator.test_timings_trie(test_port, individual_test_timings)
+
+        expected_trie = {
+          'bar.html': 0,
+          'foo': {
+              'bar': {
+                  'baz.html': 1200,
+              }
+          }
+        }
+
+        self.assertEqual(json.dumps(trie), json.dumps(expected_trie))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/layout_tests/models/__init__.py b/Tools/Scripts/webkitpy/layout_tests/models/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/models/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/layout_tests/models/result_summary.py b/Tools/Scripts/webkitpy/layout_tests/models/result_summary.py
new file mode 100644
index 0000000..5bb5010
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/models/result_summary.py
@@ -0,0 +1,82 @@
+# 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
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.layout_tests.models.test_expectations import TestExpectations, SKIP, CRASH, TIMEOUT
+
+
+class ResultSummary(object):
+    def __init__(self, expectations, test_files, iterations, expected_skips):
+        self.total = len(test_files) * iterations
+        self.remaining = self.total
+        self.expectations = expectations
+        self.expected = 0
+        self.unexpected = 0
+        self.unexpected_failures = 0
+        self.unexpected_crashes = 0
+        self.unexpected_timeouts = 0
+        self.total_tests_by_expectation = {}
+        self.tests_by_expectation = {}
+        self.tests_by_timeline = {}
+        self.results = {}
+        self.unexpected_results = {}
+        self.failures = {}
+        self.total_failures = 0
+        self.expected_skips = 0
+        self.total_tests_by_expectation[SKIP] = len(expected_skips)
+        self.tests_by_expectation[SKIP] = expected_skips
+        for expectation in TestExpectations.EXPECTATIONS.values():
+            self.tests_by_expectation[expectation] = set()
+            self.total_tests_by_expectation[expectation] = 0
+        for timeline in TestExpectations.TIMELINES.values():
+            self.tests_by_timeline[timeline] = expectations.get_tests_with_timeline(timeline)
+        self.slow_tests = set()
+
+    def add(self, test_result, expected, test_is_slow):
+        self.total_tests_by_expectation[test_result.type] += 1
+        self.tests_by_expectation[test_result.type].add(test_result.test_name)
+        self.results[test_result.test_name] = test_result
+        self.remaining -= 1
+        if len(test_result.failures):
+            self.total_failures += 1
+            self.failures[test_result.test_name] = test_result.failures
+        if expected:
+            self.expected += 1
+            if test_result.type == SKIP:
+                self.expected_skips += 1
+        else:
+            self.unexpected_results[test_result.test_name] = test_result
+            self.unexpected += 1
+            if len(test_result.failures):
+                self.unexpected_failures += 1
+            if test_result.type == CRASH:
+                self.unexpected_crashes += 1
+            elif test_result.type == TIMEOUT:
+                self.unexpected_timeouts += 1
+        if test_is_slow:
+            self.slow_tests.add(test_result.test_name)
diff --git a/Tools/Scripts/webkitpy/layout_tests/models/test_configuration.py b/Tools/Scripts/webkitpy/layout_tests/models/test_configuration.py
new file mode 100644
index 0000000..95d0f2b
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/models/test_configuration.py
@@ -0,0 +1,306 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+class TestConfiguration(object):
+    def __init__(self, version, architecture, build_type):
+        self.version = version
+        self.architecture = architecture
+        self.build_type = build_type
+
+    @classmethod
+    def category_order(cls):
+        """The most common human-readable order in which the configuration properties are listed."""
+        return ['version', 'architecture', 'build_type']
+
+    def items(self):
+        return self.__dict__.items()
+
+    def keys(self):
+        return self.__dict__.keys()
+
+    def __str__(self):
+        return ("<%(version)s, %(architecture)s, %(build_type)s>" %
+                self.__dict__)
+
+    def __repr__(self):
+        return "TestConfig(version='%(version)s', architecture='%(architecture)s', build_type='%(build_type)s')" % self.__dict__
+
+    def __hash__(self):
+        return hash(self.version + self.architecture + self.build_type)
+
+    def __eq__(self, other):
+        return self.__hash__() == other.__hash__()
+
+    def values(self):
+        """Returns the configuration values of this instance as a tuple."""
+        return self.__dict__.values()
+
+
+class SpecifierSorter(object):
+    def __init__(self, all_test_configurations=None, macros=None):
+        self._specifier_to_category = {}
+
+        if not all_test_configurations:
+            return
+        for test_configuration in all_test_configurations:
+            for category, specifier in test_configuration.items():
+                self.add_specifier(category, specifier)
+
+        self.add_macros(macros)
+
+    def add_specifier(self, category, specifier):
+        self._specifier_to_category[specifier] = category
+
+    def add_macros(self, macros):
+        if not macros:
+            return
+        # Assume well-formed macros.
+        for macro, specifier_list in macros.items():
+            self.add_specifier(self.category_for_specifier(specifier_list[0]), macro)
+
+    @classmethod
+    def category_priority(cls, category):
+        return TestConfiguration.category_order().index(category)
+
+    def specifier_priority(self, specifier):
+        return self.category_priority(self._specifier_to_category[specifier])
+
+    def category_for_specifier(self, specifier):
+        return self._specifier_to_category.get(specifier)
+
+    def sort_specifiers(self, specifiers):
+        category_slots = map(lambda x: [], TestConfiguration.category_order())
+        for specifier in specifiers:
+            category_slots[self.specifier_priority(specifier)].append(specifier)
+
+        def sort_and_return(result, specifier_list):
+            specifier_list.sort()
+            return result + specifier_list
+
+        return reduce(sort_and_return, category_slots, [])
+
+
+class TestConfigurationConverter(object):
+    def __init__(self, all_test_configurations, configuration_macros=None):
+        self._all_test_configurations = all_test_configurations
+        self._configuration_macros = configuration_macros or {}
+        self._specifier_to_configuration_set = {}
+        self._specifier_sorter = SpecifierSorter()
+        self._collapsing_sets_by_size = {}
+        self._junk_specifier_combinations = {}
+        self._collapsing_sets_by_category = {}
+        matching_sets_by_category = {}
+        for configuration in all_test_configurations:
+            for category, specifier in configuration.items():
+                self._specifier_to_configuration_set.setdefault(specifier, set()).add(configuration)
+                self._specifier_sorter.add_specifier(category, specifier)
+                self._collapsing_sets_by_category.setdefault(category, set()).add(specifier)
+                # FIXME: This seems extra-awful.
+                for cat2, spec2 in configuration.items():
+                    if category == cat2:
+                        continue
+                    matching_sets_by_category.setdefault(specifier, {}).setdefault(cat2, set()).add(spec2)
+        for collapsing_set in self._collapsing_sets_by_category.values():
+            self._collapsing_sets_by_size.setdefault(len(collapsing_set), set()).add(frozenset(collapsing_set))
+
+        for specifier, sets_by_category in matching_sets_by_category.items():
+            for category, set_by_category in sets_by_category.items():
+                if len(set_by_category) == 1 and self._specifier_sorter.category_priority(category) > self._specifier_sorter.specifier_priority(specifier):
+                    self._junk_specifier_combinations[specifier] = set_by_category
+
+        self._specifier_sorter.add_macros(configuration_macros)
+
+    def specifier_sorter(self):
+        return self._specifier_sorter
+
+    def _expand_macros(self, specifier):
+        expanded_specifiers = self._configuration_macros.get(specifier)
+        return expanded_specifiers or [specifier]
+
+    def to_config_set(self, specifier_set, error_list=None):
+        """Convert a list of specifiers into a set of TestConfiguration instances."""
+        if len(specifier_set) == 0:
+            return self._all_test_configurations
+
+        matching_sets = {}
+
+        for specifier in specifier_set:
+            for expanded_specifier in self._expand_macros(specifier):
+                configurations = self._specifier_to_configuration_set.get(expanded_specifier)
+                if not configurations:
+                    if error_list is not None:
+                        error_list.append("Unrecognized modifier '" + expanded_specifier + "'")
+                    return set()
+                category = self._specifier_sorter.category_for_specifier(expanded_specifier)
+                matching_sets.setdefault(category, set()).update(configurations)
+
+        return reduce(set.intersection, matching_sets.values())
+
+    @classmethod
+    def collapse_macros(cls, macros_dict, specifiers_list):
+        for macro_specifier, macro in macros_dict.items():
+            if len(macro) == 1:
+                continue
+
+            for combination in cls.combinations(specifiers_list, len(macro)):
+                if cls.symmetric_difference(combination) == set(macro):
+                    for item in combination:
+                        specifiers_list.remove(item)
+                    new_specifier_set = cls.intersect_combination(combination)
+                    new_specifier_set.add(macro_specifier)
+                    specifiers_list.append(frozenset(new_specifier_set))
+
+        def collapse_individual_specifier_set(macro_specifier, macro):
+            specifiers_to_remove = []
+            specifiers_to_add = []
+            for specifier_set in specifiers_list:
+                macro_set = set(macro)
+                if macro_set.intersection(specifier_set) == macro_set:
+                    specifiers_to_remove.append(specifier_set)
+                    specifiers_to_add.append(frozenset((set(specifier_set) - macro_set) | set([macro_specifier])))
+            for specifier in specifiers_to_remove:
+                specifiers_list.remove(specifier)
+            for specifier in specifiers_to_add:
+                specifiers_list.append(specifier)
+
+        for macro_specifier, macro in macros_dict.items():
+            collapse_individual_specifier_set(macro_specifier, macro)
+
+    # FIXME: itertools.combinations in buggy in Python 2.6.1 (the version that ships on SL).
+    # It seems to be okay in 2.6.5 or later; until then, this is the implementation given
+    # in http://docs.python.org/library/itertools.html (from 2.7).
+    @staticmethod
+    def combinations(iterable, r):
+        # combinations('ABCD', 2) --> AB AC AD BC BD CD
+        # combinations(range(4), 3) --> 012 013 023 123
+        pool = tuple(iterable)
+        n = len(pool)
+        if r > n:
+            return
+        indices = range(r)
+        yield tuple(pool[i] for i in indices)
+        while True:
+            for i in reversed(range(r)):
+                if indices[i] != i + n - r:
+                    break
+            else:
+                return
+            indices[i] += 1  # pylint: disable=W0631
+            for j in range(i + 1, r):  # pylint: disable=W0631
+                indices[j] = indices[j - 1] + 1
+            yield tuple(pool[i] for i in indices)
+
+    @classmethod
+    def intersect_combination(cls, combination):
+        return reduce(set.intersection, [set(specifiers) for specifiers in combination])
+
+    @classmethod
+    def symmetric_difference(cls, iterable):
+        union = set()
+        intersection = iterable[0]
+        for item in iterable:
+            union = union | item
+            intersection = intersection.intersection(item)
+        return union - intersection
+
+    def to_specifiers_list(self, test_configuration_set):
+        """Convert a set of TestConfiguration instances into one or more list of specifiers."""
+        # Easy out: if the set is all configurations, the modifier is empty.
+        if len(test_configuration_set) == len(self._all_test_configurations):
+            return [[]]
+
+        # 1) Build a list of specifier sets, discarding specifiers that don't add value.
+        specifiers_list = []
+        for config in test_configuration_set:
+            values = set(config.values())
+            for specifier, junk_specifier_set in self._junk_specifier_combinations.items():
+                if specifier in values:
+                    values -= junk_specifier_set
+            specifiers_list.append(frozenset(values))
+
+        def try_collapsing(size, collapsing_sets):
+            if len(specifiers_list) < size:
+                return False
+            for combination in self.combinations(specifiers_list, size):
+                if self.symmetric_difference(combination) in collapsing_sets:
+                    for item in combination:
+                        specifiers_list.remove(item)
+                    specifiers_list.append(frozenset(self.intersect_combination(combination)))
+                    return True
+            return False
+
+        # 2) Collapse specifier sets with common specifiers:
+        #   (xp, release), (xp, debug) --> (xp, x86)
+        for size, collapsing_sets in self._collapsing_sets_by_size.items():
+            while try_collapsing(size, collapsing_sets):
+                pass
+
+        def try_abbreviating(collapsing_sets):
+            if len(specifiers_list) < 2:
+                return False
+            for combination in self.combinations(specifiers_list, 2):
+                for collapsing_set in collapsing_sets:
+                    diff = self.symmetric_difference(combination)
+                    if diff <= collapsing_set:
+                        common = self.intersect_combination(combination)
+                        for item in combination:
+                            specifiers_list.remove(item)
+                        specifiers_list.append(frozenset(common | diff))
+                        return True
+            return False
+
+        # 3) Abbreviate specifier sets by combining specifiers across categories.
+        #   (xp, release), (win7, release) --> (xp, win7, release)
+        while try_abbreviating(self._collapsing_sets_by_size.values()):
+            pass
+
+
+        # 4) Substitute specifier subsets that match macros witin each set:
+        #   (xp, vista, win7, release) -> (win, release)
+        self.collapse_macros(self._configuration_macros, specifiers_list)
+
+        macro_keys = set(self._configuration_macros.keys())
+
+        # 5) Collapsing macros may have created combinations the can now be abbreviated.
+        #   (xp, release), (linux, x86, release), (linux, x86_64, release) --> (xp, release), (linux, release) --> (xp, linux, release)
+        while try_abbreviating([self._collapsing_sets_by_category['version'] | macro_keys]):
+            pass
+
+        # 6) Remove cases where we have collapsed but have all macros.
+        #   (android, win, mac, linux, release) --> (release)
+        specifiers_to_remove = []
+        for specifier_set in specifiers_list:
+            if macro_keys <= specifier_set:
+                specifiers_to_remove.append(specifier_set)
+
+        for specifier_set in specifiers_to_remove:
+            specifiers_list.remove(specifier_set)
+            specifiers_list.append(frozenset(specifier_set - macro_keys))
+
+        return specifiers_list
diff --git a/Tools/Scripts/webkitpy/layout_tests/models/test_configuration_unittest.py b/Tools/Scripts/webkitpy/layout_tests/models/test_configuration_unittest.py
new file mode 100644
index 0000000..5c43b6a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/models/test_configuration_unittest.py
@@ -0,0 +1,369 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.layout_tests.models.test_configuration import *
+
+
+def make_mock_all_test_configurations_set():
+    all_test_configurations = set()
+    for version, architecture in (('snowleopard', 'x86'), ('xp', 'x86'), ('win7', 'x86'), ('vista', 'x86'), ('lucid', 'x86'), ('lucid', 'x86_64')):
+        for build_type in ('debug', 'release'):
+            all_test_configurations.add(TestConfiguration(version, architecture, build_type))
+    return all_test_configurations
+
+MOCK_MACROS = {
+    'mac': ['snowleopard'],
+    'win': ['xp', 'vista', 'win7'],
+    'linux': ['lucid'],
+}
+
+
+class TestConfigurationTest(unittest.TestCase):
+    def test_items(self):
+        config = TestConfiguration('xp', 'x86', 'release')
+        result_config_dict = {}
+        for category, specifier in config.items():
+            result_config_dict[category] = specifier
+        self.assertEquals({'version': 'xp', 'architecture': 'x86', 'build_type': 'release'}, result_config_dict)
+
+    def test_keys(self):
+        config = TestConfiguration('xp', 'x86', 'release')
+        result_config_keys = []
+        for category in config.keys():
+            result_config_keys.append(category)
+        self.assertEquals(set(['version', 'architecture', 'build_type']), set(result_config_keys))
+
+    def test_str(self):
+        config = TestConfiguration('xp', 'x86', 'release')
+        self.assertEquals('<xp, x86, release>', str(config))
+
+    def test_repr(self):
+        config = TestConfiguration('xp', 'x86', 'release')
+        self.assertEquals("TestConfig(version='xp', architecture='x86', build_type='release')", repr(config))
+
+    def test_hash(self):
+        config_dict = {}
+        config_dict[TestConfiguration('xp', 'x86', 'release')] = True
+        self.assertTrue(TestConfiguration('xp', 'x86', 'release') in config_dict)
+        self.assertTrue(config_dict[TestConfiguration('xp', 'x86', 'release')])
+
+        def query_unknown_key():
+            return config_dict[TestConfiguration('xp', 'x86', 'debug')]
+
+        self.assertRaises(KeyError, query_unknown_key)
+        self.assertTrue(TestConfiguration('xp', 'x86', 'release') in config_dict)
+        self.assertFalse(TestConfiguration('xp', 'x86', 'debug') in config_dict)
+        configs_list = [TestConfiguration('xp', 'x86', 'release'), TestConfiguration('xp', 'x86', 'debug'), TestConfiguration('xp', 'x86', 'debug')]
+        self.assertEquals(len(configs_list), 3)
+        self.assertEquals(len(set(configs_list)), 2)
+
+    def test_eq(self):
+        self.assertEquals(TestConfiguration('xp', 'x86', 'release'), TestConfiguration('xp', 'x86', 'release'))
+        self.assertNotEquals(TestConfiguration('xp', 'x86', 'release'), TestConfiguration('xp', 'x86', 'debug'))
+
+    def test_values(self):
+        config = TestConfiguration('xp', 'x86', 'release')
+        result_config_values = []
+        for value in config.values():
+            result_config_values.append(value)
+        self.assertEquals(set(['xp', 'x86', 'release']), set(result_config_values))
+
+
+class SpecifierSorterTest(unittest.TestCase):
+    def __init__(self, testFunc):
+        self._all_test_configurations = make_mock_all_test_configurations_set()
+        unittest.TestCase.__init__(self, testFunc)
+
+    def test_init(self):
+        sorter = SpecifierSorter()
+        self.assertEquals(sorter.category_for_specifier('control'), None)
+        sorter = SpecifierSorter(self._all_test_configurations)
+        self.assertEquals(sorter.category_for_specifier('xp'), 'version')
+        sorter = SpecifierSorter(self._all_test_configurations, MOCK_MACROS)
+        self.assertEquals(sorter.category_for_specifier('mac'), 'version')
+
+    def test_add_specifier(self):
+        sorter = SpecifierSorter()
+        self.assertEquals(sorter.category_for_specifier('control'), None)
+        sorter.add_specifier('version', 'control')
+        self.assertEquals(sorter.category_for_specifier('control'), 'version')
+        sorter.add_specifier('version', 'one')
+        self.assertEquals(sorter.category_for_specifier('one'), 'version')
+        sorter.add_specifier('architecture', 'renaissance')
+        self.assertEquals(sorter.category_for_specifier('one'), 'version')
+        self.assertEquals(sorter.category_for_specifier('renaissance'), 'architecture')
+
+    def test_add_macros(self):
+        sorter = SpecifierSorter(self._all_test_configurations)
+        sorter.add_macros(MOCK_MACROS)
+        self.assertEquals(sorter.category_for_specifier('mac'), 'version')
+        self.assertEquals(sorter.category_for_specifier('win'), 'version')
+        self.assertEquals(sorter.category_for_specifier('x86'), 'architecture')
+
+    def test_category_priority(self):
+        sorter = SpecifierSorter(self._all_test_configurations)
+        self.assertEquals(sorter.category_priority('version'), 0)
+        self.assertEquals(sorter.category_priority('build_type'), 2)
+
+    def test_specifier_priority(self):
+        sorter = SpecifierSorter(self._all_test_configurations)
+        self.assertEquals(sorter.specifier_priority('x86'), 1)
+        self.assertEquals(sorter.specifier_priority('snowleopard'), 0)
+
+    def test_sort_specifiers(self):
+        sorter = SpecifierSorter(self._all_test_configurations, MOCK_MACROS)
+        self.assertEquals(sorter.sort_specifiers(set()), [])
+        self.assertEquals(sorter.sort_specifiers(set(['x86'])), ['x86'])
+        self.assertEquals(sorter.sort_specifiers(set(['x86', 'win7'])), ['win7', 'x86'])
+        self.assertEquals(sorter.sort_specifiers(set(['x86', 'debug', 'win7'])), ['win7', 'x86', 'debug'])
+        self.assertEquals(sorter.sort_specifiers(set(['snowleopard', 'x86', 'debug', 'win7'])), ['snowleopard', 'win7', 'x86', 'debug'])
+        self.assertEquals(sorter.sort_specifiers(set(['x86', 'mac', 'debug', 'win7'])), ['mac', 'win7', 'x86', 'debug'])
+
+
+class TestConfigurationConverterTest(unittest.TestCase):
+    def __init__(self, testFunc):
+        self._all_test_configurations = make_mock_all_test_configurations_set()
+        unittest.TestCase.__init__(self, testFunc)
+
+    def test_symmetric_difference(self):
+        self.assertEquals(TestConfigurationConverter.symmetric_difference([set(['a', 'b']), set(['b', 'c'])]), set(['a', 'c']))
+        self.assertEquals(TestConfigurationConverter.symmetric_difference([set(['a', 'b']), set(['b', 'c']), set(['b', 'd'])]), set(['a', 'c', 'd']))
+
+    def test_to_config_set(self):
+        converter = TestConfigurationConverter(self._all_test_configurations)
+
+        self.assertEquals(converter.to_config_set(set()), self._all_test_configurations)
+
+        self.assertEquals(converter.to_config_set(set(['foo'])), set())
+
+        self.assertEquals(converter.to_config_set(set(['xp', 'foo'])), set())
+
+        errors = []
+        self.assertEquals(converter.to_config_set(set(['xp', 'foo']), errors), set())
+        self.assertEquals(errors, ["Unrecognized modifier 'foo'"])
+
+        self.assertEquals(converter.to_config_set(set(['xp', 'x86_64'])), set())
+
+        configs_to_match = set([
+            TestConfiguration('xp', 'x86', 'release'),
+        ])
+        self.assertEquals(converter.to_config_set(set(['xp', 'release'])), configs_to_match)
+
+        configs_to_match = set([
+            TestConfiguration('snowleopard', 'x86', 'release'),
+            TestConfiguration('vista', 'x86', 'release'),
+            TestConfiguration('win7', 'x86', 'release'),
+            TestConfiguration('xp', 'x86', 'release'),
+            TestConfiguration('lucid', 'x86', 'release'),
+            TestConfiguration('lucid', 'x86_64', 'release'),
+       ])
+        self.assertEquals(converter.to_config_set(set(['release'])), configs_to_match)
+
+        configs_to_match = set([
+             TestConfiguration('lucid', 'x86_64', 'release'),
+             TestConfiguration('lucid', 'x86_64', 'debug'),
+        ])
+        self.assertEquals(converter.to_config_set(set(['x86_64'])), configs_to_match)
+
+        configs_to_match = set([
+            TestConfiguration('lucid', 'x86_64', 'release'),
+            TestConfiguration('lucid', 'x86_64', 'debug'),
+            TestConfiguration('lucid', 'x86', 'release'),
+            TestConfiguration('lucid', 'x86', 'debug'),
+            TestConfiguration('snowleopard', 'x86', 'release'),
+            TestConfiguration('snowleopard', 'x86', 'debug'),
+        ])
+        self.assertEquals(converter.to_config_set(set(['lucid', 'snowleopard'])), configs_to_match)
+
+        configs_to_match = set([
+            TestConfiguration('lucid', 'x86', 'release'),
+            TestConfiguration('lucid', 'x86', 'debug'),
+            TestConfiguration('snowleopard', 'x86', 'release'),
+            TestConfiguration('snowleopard', 'x86', 'debug'),
+        ])
+        self.assertEquals(converter.to_config_set(set(['lucid', 'snowleopard', 'x86'])), configs_to_match)
+
+        configs_to_match = set([
+            TestConfiguration('lucid', 'x86_64', 'release'),
+            TestConfiguration('lucid', 'x86', 'release'),
+            TestConfiguration('snowleopard', 'x86', 'release'),
+        ])
+        self.assertEquals(converter.to_config_set(set(['lucid', 'snowleopard', 'release'])), configs_to_match)
+
+    def test_macro_expansion(self):
+        converter = TestConfigurationConverter(self._all_test_configurations, MOCK_MACROS)
+
+        configs_to_match = set([
+            TestConfiguration('xp', 'x86', 'release'),
+            TestConfiguration('vista', 'x86', 'release'),
+            TestConfiguration('win7', 'x86', 'release'),
+        ])
+        self.assertEquals(converter.to_config_set(set(['win', 'release'])), configs_to_match)
+
+        configs_to_match = set([
+            TestConfiguration('xp', 'x86', 'release'),
+            TestConfiguration('vista', 'x86', 'release'),
+            TestConfiguration('win7', 'x86', 'release'),
+            TestConfiguration('lucid', 'x86', 'release'),
+            TestConfiguration('lucid', 'x86_64', 'release'),
+        ])
+        self.assertEquals(converter.to_config_set(set(['win', 'lucid', 'release'])), configs_to_match)
+
+        configs_to_match = set([
+            TestConfiguration('xp', 'x86', 'release'),
+            TestConfiguration('vista', 'x86', 'release'),
+            TestConfiguration('win7', 'x86', 'release'),
+            TestConfiguration('snowleopard', 'x86', 'release'),
+        ])
+        self.assertEquals(converter.to_config_set(set(['win', 'mac', 'release'])), configs_to_match)
+
+    def test_to_specifier_lists(self):
+        converter = TestConfigurationConverter(self._all_test_configurations, MOCK_MACROS)
+
+        self.assertEquals(converter.to_specifiers_list(set(self._all_test_configurations)), [[]])
+        self.assertEquals(converter.to_specifiers_list(set()), [])
+
+        configs_to_match = set([
+            TestConfiguration('xp', 'x86', 'release'),
+        ])
+        self.assertEquals(converter.to_specifiers_list(configs_to_match), [set(['release', 'xp'])])
+
+        configs_to_match = set([
+            TestConfiguration('xp', 'x86', 'release'),
+            TestConfiguration('xp', 'x86', 'debug'),
+        ])
+        self.assertEquals(converter.to_specifiers_list(configs_to_match), [set(['xp'])])
+
+        configs_to_match = set([
+            TestConfiguration('lucid', 'x86_64', 'debug'),
+            TestConfiguration('xp', 'x86', 'release'),
+        ])
+        self.assertEquals(converter.to_specifiers_list(configs_to_match), [set(['release', 'xp']), set(['debug', 'x86_64', 'linux'])])
+
+        configs_to_match = set([
+            TestConfiguration('xp', 'x86', 'release'),
+            TestConfiguration('xp', 'x86', 'release'),
+            TestConfiguration('lucid', 'x86_64', 'debug'),
+            TestConfiguration('lucid', 'x86', 'debug'),
+            TestConfiguration('lucid', 'x86_64', 'debug'),
+            TestConfiguration('lucid', 'x86', 'debug'),
+        ])
+        self.assertEquals(converter.to_specifiers_list(configs_to_match), [set(['release', 'xp']), set(['debug', 'linux'])])
+
+        configs_to_match = set([
+            TestConfiguration('xp', 'x86', 'release'),
+            TestConfiguration('snowleopard', 'x86', 'release'),
+            TestConfiguration('vista', 'x86', 'release'),
+            TestConfiguration('win7', 'x86', 'release'),
+            TestConfiguration('lucid', 'x86', 'release'),
+            TestConfiguration('lucid', 'x86_64', 'release'),
+        ])
+        self.assertEquals(converter.to_specifiers_list(configs_to_match), [set(['release'])])
+
+        configs_to_match = set([
+            TestConfiguration('xp', 'x86', 'release'),
+            TestConfiguration('snowleopard', 'x86', 'release'),
+        ])
+        self.assertEquals(converter.to_specifiers_list(configs_to_match), [set(['xp', 'mac', 'release'])])
+
+        configs_to_match = set([
+            TestConfiguration('xp', 'x86', 'release'),
+            TestConfiguration('snowleopard', 'x86', 'release'),
+            TestConfiguration('win7', 'x86', 'release'),
+            TestConfiguration('win7', 'x86', 'debug'),
+            TestConfiguration('lucid', 'x86', 'release'),
+        ])
+        self.assertEquals(converter.to_specifiers_list(configs_to_match), [set(['win7']), set(['release', 'linux', 'x86']), set(['release', 'xp', 'mac'])])
+
+    def test_macro_collapsing(self):
+        macros = {'foo': ['bar', 'baz'], 'people': ['bob', 'alice', 'john']}
+
+        specifiers_list = [set(['john', 'godzilla', 'bob', 'alice'])]
+        TestConfigurationConverter.collapse_macros(macros, specifiers_list)
+        self.assertEquals(specifiers_list, [set(['people', 'godzilla'])])
+
+        specifiers_list = [set(['john', 'godzilla', 'alice'])]
+        TestConfigurationConverter.collapse_macros(macros, specifiers_list)
+        self.assertEquals(specifiers_list, [set(['john', 'godzilla', 'alice', 'godzilla'])])
+
+        specifiers_list = [set(['bar', 'godzilla', 'baz', 'bob', 'alice', 'john'])]
+        TestConfigurationConverter.collapse_macros(macros, specifiers_list)
+        self.assertEquals(specifiers_list, [set(['foo', 'godzilla', 'people'])])
+
+        specifiers_list = [set(['bar', 'godzilla', 'baz', 'bob']), set(['bar', 'baz']), set(['people', 'alice', 'bob', 'john'])]
+        TestConfigurationConverter.collapse_macros(macros, specifiers_list)
+        self.assertEquals(specifiers_list, [set(['bob', 'foo', 'godzilla']), set(['foo']), set(['people'])])
+
+    def test_converter_macro_collapsing(self):
+        converter = TestConfigurationConverter(self._all_test_configurations, MOCK_MACROS)
+
+        configs_to_match = set([
+            TestConfiguration('xp', 'x86', 'release'),
+            TestConfiguration('vista', 'x86', 'release'),
+            TestConfiguration('win7', 'x86', 'release'),
+        ])
+        self.assertEquals(converter.to_specifiers_list(configs_to_match), [set(['win', 'release'])])
+
+        configs_to_match = set([
+            TestConfiguration('xp', 'x86', 'release'),
+            TestConfiguration('vista', 'x86', 'release'),
+            TestConfiguration('win7', 'x86', 'release'),
+            TestConfiguration('lucid', 'x86', 'release'),
+            TestConfiguration('lucid', 'x86_64', 'release'),
+        ])
+        self.assertEquals(converter.to_specifiers_list(configs_to_match), [set(['win', 'linux', 'release'])])
+
+        configs_to_match = set([
+            TestConfiguration('xp', 'x86', 'release'),
+            TestConfiguration('vista', 'x86', 'release'),
+            TestConfiguration('win7', 'x86', 'release'),
+            TestConfiguration('snowleopard', 'x86', 'release'),
+        ])
+        self.assertEquals(converter.to_specifiers_list(configs_to_match), [set(['win', 'mac', 'release'])])
+
+        configs_to_match = set([
+            TestConfiguration('xp', 'x86', 'release'),
+            TestConfiguration('vista', 'x86', 'release'),
+            TestConfiguration('win7', 'x86', 'release'),
+            TestConfiguration('snowleopard', 'x86', 'release'),
+        ])
+        self.assertEquals(converter.to_specifiers_list(configs_to_match), [set(['win', 'mac', 'release'])])
+
+        configs_to_match = set([
+            TestConfiguration('xp', 'x86', 'release'),
+            TestConfiguration('vista', 'x86', 'release'),
+            TestConfiguration('win7', 'x86', 'release'),
+        ])
+        self.assertEquals(converter.to_specifiers_list(configs_to_match), [set(['win', 'release'])])
+
+    def test_specifier_converter_access(self):
+        specifier_sorter = TestConfigurationConverter(self._all_test_configurations, MOCK_MACROS).specifier_sorter()
+        self.assertEquals(specifier_sorter.category_for_specifier('snowleopard'), 'version')
+        self.assertEquals(specifier_sorter.category_for_specifier('mac'), 'version')
diff --git a/Tools/Scripts/webkitpy/layout_tests/models/test_expectations.py b/Tools/Scripts/webkitpy/layout_tests/models/test_expectations.py
new file mode 100644
index 0000000..2342596
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/models/test_expectations.py
@@ -0,0 +1,1013 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""A helper class for reading in and dealing with tests expectations
+for layout tests.
+"""
+
+import logging
+import re
+
+from webkitpy.layout_tests.models.test_configuration import TestConfigurationConverter
+
+_log = logging.getLogger(__name__)
+
+
+# Test expectation and modifier constants.
+#
+# FIXME: range() starts with 0 which makes if expectation checks harder
+# as PASS is 0.
+(PASS, FAIL, TEXT, IMAGE, IMAGE_PLUS_TEXT, AUDIO, TIMEOUT, CRASH, SKIP, WONTFIX,
+ SLOW, REBASELINE, MISSING, FLAKY, NOW, NONE) = range(16)
+
+# FIXME: Perhas these two routines should be part of the Port instead?
+BASELINE_SUFFIX_LIST = ('png', 'wav', 'txt')
+
+
+class ParseError(Exception):
+    def __init__(self, warnings):
+        super(ParseError, self).__init__()
+        self.warnings = warnings
+
+    def __str__(self):
+        return '\n'.join(map(str, self.warnings))
+
+    def __repr__(self):
+        return 'ParseError(warnings=%s)' % self.warnings
+
+
+class TestExpectationParser(object):
+    """Provides parsing facilities for lines in the test_expectation.txt file."""
+
+    DUMMY_BUG_MODIFIER = "bug_dummy"
+    BUG_MODIFIER_PREFIX = 'bug'
+    BUG_MODIFIER_REGEX = 'bug\d+'
+    REBASELINE_MODIFIER = 'rebaseline'
+    PASS_EXPECTATION = 'pass'
+    SKIP_MODIFIER = 'skip'
+    SLOW_MODIFIER = 'slow'
+    WONTFIX_MODIFIER = 'wontfix'
+
+    TIMEOUT_EXPECTATION = 'timeout'
+
+    MISSING_BUG_WARNING = 'Test lacks BUG modifier.'
+
+    def __init__(self, port, full_test_list, allow_rebaseline_modifier):
+        self._port = port
+        self._test_configuration_converter = TestConfigurationConverter(set(port.all_test_configurations()), port.configuration_specifier_macros())
+        self._full_test_list = full_test_list
+        self._allow_rebaseline_modifier = allow_rebaseline_modifier
+
+    def parse(self, filename, expectations_string):
+        expectation_lines = []
+        line_number = 0
+        for line in expectations_string.split("\n"):
+            line_number += 1
+            test_expectation = self._tokenize_line(filename, line, line_number)
+            self._parse_line(test_expectation)
+            expectation_lines.append(test_expectation)
+        return expectation_lines
+
+    def expectation_for_skipped_test(self, test_name):
+        if not self._port.test_exists(test_name):
+            _log.warning('The following test %s from the Skipped list doesn\'t exist' % test_name)
+        expectation_line = TestExpectationLine()
+        expectation_line.original_string = test_name
+        expectation_line.modifiers = [TestExpectationParser.DUMMY_BUG_MODIFIER, TestExpectationParser.SKIP_MODIFIER]
+        # FIXME: It's not clear what the expectations for a skipped test should be; the expectations
+        # might be different for different entries in a Skipped file, or from the command line, or from
+        # only running parts of the tests. It's also not clear if it matters much.
+        expectation_line.modifiers.append(TestExpectationParser.WONTFIX_MODIFIER)
+        expectation_line.name = test_name
+        # FIXME: we should pass in a more descriptive string here.
+        expectation_line.filename = '<Skipped file>'
+        expectation_line.line_number = 0
+        expectation_line.expectations = [TestExpectationParser.PASS_EXPECTATION]
+        self._parse_line(expectation_line)
+        return expectation_line
+
+    def _parse_line(self, expectation_line):
+        if not expectation_line.name:
+            return
+
+        if not self._check_test_exists(expectation_line):
+            return
+
+        expectation_line.is_file = self._port.test_isfile(expectation_line.name)
+        if expectation_line.is_file:
+            expectation_line.path = expectation_line.name
+        else:
+            expectation_line.path = self._port.normalize_test_name(expectation_line.name)
+
+        self._collect_matching_tests(expectation_line)
+
+        self._parse_modifiers(expectation_line)
+        self._parse_expectations(expectation_line)
+
+    def _parse_modifiers(self, expectation_line):
+        has_wontfix = False
+        has_bugid = False
+        parsed_specifiers = set()
+
+        modifiers = [modifier.lower() for modifier in expectation_line.modifiers]
+        expectations = [expectation.lower() for expectation in expectation_line.expectations]
+
+        if self.SLOW_MODIFIER in modifiers and self.TIMEOUT_EXPECTATION in expectations:
+            expectation_line.warnings.append('A test can not be both SLOW and TIMEOUT. If it times out indefinitely, then it should be just TIMEOUT.')
+
+        for modifier in modifiers:
+            if modifier in TestExpectations.MODIFIERS:
+                expectation_line.parsed_modifiers.append(modifier)
+                if modifier == self.WONTFIX_MODIFIER:
+                    has_wontfix = True
+            elif modifier.startswith(self.BUG_MODIFIER_PREFIX):
+                has_bugid = True
+                if re.match(self.BUG_MODIFIER_REGEX, modifier):
+                    expectation_line.warnings.append('BUG\d+ is not allowed, must be one of BUGCR\d+, BUGWK\d+, BUGV8_\d+, or a non-numeric bug identifier.')
+                else:
+                    expectation_line.parsed_bug_modifiers.append(modifier)
+            else:
+                parsed_specifiers.add(modifier)
+
+        if not expectation_line.parsed_bug_modifiers and not has_wontfix and not has_bugid and self._port.warn_if_bug_missing_in_test_expectations():
+            expectation_line.warnings.append(self.MISSING_BUG_WARNING)
+
+        if self._allow_rebaseline_modifier and self.REBASELINE_MODIFIER in modifiers:
+            expectation_line.warnings.append('REBASELINE should only be used for running rebaseline.py. Cannot be checked in.')
+
+        expectation_line.matching_configurations = self._test_configuration_converter.to_config_set(parsed_specifiers, expectation_line.warnings)
+
+    def _parse_expectations(self, expectation_line):
+        result = set()
+        for part in expectation_line.expectations:
+            expectation = TestExpectations.expectation_from_string(part)
+            if expectation is None:  # Careful, PASS is currently 0.
+                expectation_line.warnings.append('Unsupported expectation: %s' % part)
+                continue
+            result.add(expectation)
+        expectation_line.parsed_expectations = result
+
+    def _check_test_exists(self, expectation_line):
+        # WebKit's way of skipping tests is to add a -disabled suffix.
+        # So we should consider the path existing if the path or the
+        # -disabled version exists.
+        if not self._port.test_exists(expectation_line.name) and not self._port.test_exists(expectation_line.name + '-disabled'):
+            # Log a warning here since you hit this case any
+            # time you update TestExpectations without syncing
+            # the LayoutTests directory
+            expectation_line.warnings.append('Path does not exist.')
+            return False
+        return True
+
+    def _collect_matching_tests(self, expectation_line):
+        """Convert the test specification to an absolute, normalized
+        path and make sure directories end with the OS path separator."""
+        # FIXME: full_test_list can quickly contain a big amount of
+        # elements. We should consider at some point to use a more
+        # efficient structure instead of a list. Maybe a dictionary of
+        # lists to represent the tree of tests, leaves being test
+        # files and nodes being categories.
+
+        if not self._full_test_list:
+            expectation_line.matching_tests = [expectation_line.path]
+            return
+
+        if not expectation_line.is_file:
+            # this is a test category, return all the tests of the category.
+            expectation_line.matching_tests = [test for test in self._full_test_list if test.startswith(expectation_line.path)]
+            return
+
+        # this is a test file, do a quick check if it's in the
+        # full test suite.
+        if expectation_line.path in self._full_test_list:
+            expectation_line.matching_tests.append(expectation_line.path)
+
+    # FIXME: Update the original modifiers and remove this once the old syntax is gone.
+    _configuration_tokens_list = [
+        'Mac', 'SnowLeopard', 'Lion', 'MountainLion',
+        'Win', 'XP', 'Vista', 'Win7',
+        'Linux',
+        'Android',
+        'Release',
+        'Debug',
+    ]
+
+    _configuration_tokens = dict((token, token.upper()) for token in _configuration_tokens_list)
+    _inverted_configuration_tokens = dict((value, name) for name, value in _configuration_tokens.iteritems())
+
+    # FIXME: Update the original modifiers list and remove this once the old syntax is gone.
+    _expectation_tokens = {
+        'Crash': 'CRASH',
+        'Failure': 'FAIL',
+        'ImageOnlyFailure': 'IMAGE',
+        'Missing': 'MISSING',
+        'Pass': 'PASS',
+        'Rebaseline': 'REBASELINE',
+        'Skip': 'SKIP',
+        'Slow': 'SLOW',
+        'Timeout': 'TIMEOUT',
+        'WontFix': 'WONTFIX',
+    }
+
+    _inverted_expectation_tokens = dict([(value, name) for name, value in _expectation_tokens.iteritems()] +
+                                        [('TEXT', 'Failure'), ('IMAGE+TEXT', 'Failure'), ('AUDIO', 'Failure')])
+
+    # FIXME: Seems like these should be classmethods on TestExpectationLine instead of TestExpectationParser.
+    @classmethod
+    def _tokenize_line(cls, filename, expectation_string, line_number):
+        """Tokenizes a line from TestExpectations and returns an unparsed TestExpectationLine instance using the old format.
+
+        The new format for a test expectation line is:
+
+        [[bugs] [ "[" <configuration modifiers> "]" <name> [ "[" <expectations> "]" ["#" <comment>]
+
+        Any errant whitespace is not preserved.
+
+        """
+        expectation_line = TestExpectationLine()
+        expectation_line.original_string = expectation_string
+        expectation_line.filename = filename
+        expectation_line.line_number = line_number
+
+        comment_index = expectation_string.find("#")
+        if comment_index == -1:
+            comment_index = len(expectation_string)
+        else:
+            expectation_line.comment = expectation_string[comment_index + 1:]
+
+        remaining_string = re.sub(r"\s+", " ", expectation_string[:comment_index].strip())
+        if len(remaining_string) == 0:
+            return expectation_line
+
+        # special-case parsing this so that we fail immediately instead of treating this as a test name
+        if remaining_string.startswith('//'):
+            expectation_line.warnings = ['use "#" instead of "//" for comments']
+            return expectation_line
+
+        bugs = []
+        modifiers = []
+        name = None
+        expectations = []
+        warnings = []
+
+        WEBKIT_BUG_PREFIX = 'webkit.org/b/'
+        CHROMIUM_BUG_PREFIX = 'crbug.com/'
+        V8_BUG_PREFIX = 'code.google.com/p/v8/issues/detail?id='
+
+        tokens = remaining_string.split()
+        state = 'start'
+        for token in tokens:
+            if (token.startswith(WEBKIT_BUG_PREFIX) or
+                token.startswith(CHROMIUM_BUG_PREFIX) or
+                token.startswith(V8_BUG_PREFIX) or
+                token.startswith('Bug(')):
+                if state != 'start':
+                    warnings.append('"%s" is not at the start of the line.' % token)
+                    break
+                if token.startswith(WEBKIT_BUG_PREFIX):
+                    bugs.append(token.replace(WEBKIT_BUG_PREFIX, 'BUGWK'))
+                elif token.startswith(CHROMIUM_BUG_PREFIX):
+                    bugs.append(token.replace(CHROMIUM_BUG_PREFIX, 'BUGCR'))
+                elif token.startswith(V8_BUG_PREFIX):
+                    bugs.append(token.replace(V8_BUG_PREFIX, 'BUGV8_'))
+                else:
+                    match = re.match('Bug\((\w+)\)$', token)
+                    if not match:
+                        warnings.append('unrecognized bug identifier "%s"' % token)
+                        break
+                    else:
+                        bugs.append('BUG' + match.group(1).upper())
+            elif token.startswith('BUG'):
+                warnings.append('unrecognized old-style bug identifier "%s"' % token)
+                break
+            elif token == '[':
+                if state == 'start':
+                    state = 'configuration'
+                elif state == 'name_found':
+                    state = 'expectations'
+                else:
+                    warnings.append('unexpected "["')
+                    break
+            elif token == ']':
+                if state == 'configuration':
+                    state = 'name'
+                elif state == 'expectations':
+                    state = 'done'
+                else:
+                    warnings.append('unexpected "]"')
+                    break
+            elif token in ('//', ':', '='):
+                warnings.append('"%s" is not legal in the new TestExpectations syntax.' % token)
+                break
+            elif state == 'configuration':
+                modifiers.append(cls._configuration_tokens.get(token, token))
+            elif state == 'expectations':
+                if token in ('Rebaseline', 'Skip', 'Slow', 'WontFix'):
+                    modifiers.append(token.upper())
+                else:
+                    expectations.append(cls._expectation_tokens.get(token, token))
+            elif state == 'name_found':
+                warnings.append('expecting "[", "#", or end of line instead of "%s"' % token)
+                break
+            else:
+                name = token
+                state = 'name_found'
+
+        if not warnings:
+            if not name:
+                warnings.append('Did not find a test name.')
+            elif state not in ('name_found', 'done'):
+                warnings.append('Missing a "]"')
+
+        if 'WONTFIX' in modifiers and 'SKIP' not in modifiers:
+            modifiers.append('SKIP')
+
+        if 'SKIP' in modifiers and expectations:
+            # FIXME: This is really a semantic warning and shouldn't be here. Remove when we drop the old syntax.
+            warnings.append('A test marked Skip or WontFix must not have other expectations.')
+        elif not expectations:
+            if 'SKIP' not in modifiers and 'REBASELINE' not in modifiers and 'SLOW' not in modifiers:
+                modifiers.append('SKIP')
+            expectations = ['PASS']
+
+        # FIXME: expectation line should just store bugs and modifiers separately.
+        expectation_line.modifiers = bugs + modifiers
+        expectation_line.expectations = expectations
+        expectation_line.name = name
+        expectation_line.warnings = warnings
+        return expectation_line
+
+    @classmethod
+    def _split_space_separated(cls, space_separated_string):
+        """Splits a space-separated string into an array."""
+        return [part.strip() for part in space_separated_string.strip().split(' ')]
+
+
+class TestExpectationLine(object):
+    """Represents a line in test expectations file."""
+
+    def __init__(self):
+        """Initializes a blank-line equivalent of an expectation."""
+        self.original_string = None
+        self.filename = None  # this is the path to the expectations file for this line
+        self.line_number = None
+        self.name = None  # this is the path in the line itself
+        self.path = None  # this is the normpath of self.name
+        self.modifiers = []
+        self.parsed_modifiers = []
+        self.parsed_bug_modifiers = []
+        self.matching_configurations = set()
+        self.expectations = []
+        self.parsed_expectations = set()
+        self.comment = None
+        self.matching_tests = []
+        self.warnings = []
+
+    def is_invalid(self):
+        return self.warnings and self.warnings != [TestExpectationParser.MISSING_BUG_WARNING]
+
+    def is_flaky(self):
+        return len(self.parsed_expectations) > 1
+
+    @staticmethod
+    def create_passing_expectation(test):
+        expectation_line = TestExpectationLine()
+        expectation_line.name = test
+        expectation_line.path = test
+        expectation_line.parsed_expectations = set([PASS])
+        expectation_line.expectations = set(['PASS'])
+        expectation_line.matching_tests = [test]
+        return expectation_line
+
+    def to_string(self, test_configuration_converter, include_modifiers=True, include_expectations=True, include_comment=True):
+        parsed_expectation_to_string = dict([[parsed_expectation, expectation_string] for expectation_string, parsed_expectation in TestExpectations.EXPECTATIONS.items()])
+
+        if self.is_invalid():
+            return self.original_string or ''
+
+        if self.name is None:
+            return '' if self.comment is None else "#%s" % self.comment
+
+        if test_configuration_converter and self.parsed_bug_modifiers:
+            specifiers_list = test_configuration_converter.to_specifiers_list(self.matching_configurations)
+            result = []
+            for specifiers in specifiers_list:
+                # FIXME: this is silly that we join the modifiers and then immediately split them.
+                modifiers = self._serialize_parsed_modifiers(test_configuration_converter, specifiers).split()
+                expectations = self._serialize_parsed_expectations(parsed_expectation_to_string).split()
+                result.append(self._format_line(modifiers, self.name, expectations, self.comment))
+            return "\n".join(result) if result else None
+
+        return self._format_line(self.modifiers, self.name, self.expectations, self.comment,
+            include_modifiers, include_expectations, include_comment)
+
+    def to_csv(self):
+        # Note that this doesn't include the comments.
+        return '%s,%s,%s' % (self.name, ' '.join(self.modifiers), ' '.join(self.expectations))
+
+    def _serialize_parsed_expectations(self, parsed_expectation_to_string):
+        result = []
+        for index in TestExpectations.EXPECTATION_ORDER:
+            if index in self.parsed_expectations:
+                result.append(parsed_expectation_to_string[index])
+        return ' '.join(result)
+
+    def _serialize_parsed_modifiers(self, test_configuration_converter, specifiers):
+        result = []
+        if self.parsed_bug_modifiers:
+            result.extend(sorted(self.parsed_bug_modifiers))
+        result.extend(sorted(self.parsed_modifiers))
+        result.extend(test_configuration_converter.specifier_sorter().sort_specifiers(specifiers))
+        return ' '.join(result)
+
+    @staticmethod
+    def _format_line(modifiers, name, expectations, comment, include_modifiers=True, include_expectations=True, include_comment=True):
+        bugs = []
+        new_modifiers = []
+        new_expectations = []
+        for modifier in modifiers:
+            modifier = modifier.upper()
+            if modifier.startswith('BUGWK'):
+                bugs.append('webkit.org/b/' + modifier.replace('BUGWK', ''))
+            elif modifier.startswith('BUGCR'):
+                bugs.append('crbug.com/' + modifier.replace('BUGCR', ''))
+            elif modifier.startswith('BUG'):
+                # FIXME: we should preserve case once we can drop the old syntax.
+                bugs.append('Bug(' + modifier[3:].lower() + ')')
+            elif modifier in ('SLOW', 'SKIP', 'REBASELINE', 'WONTFIX'):
+                new_expectations.append(TestExpectationParser._inverted_expectation_tokens.get(modifier))
+            else:
+                new_modifiers.append(TestExpectationParser._inverted_configuration_tokens.get(modifier, modifier))
+
+        for expectation in expectations:
+            expectation = expectation.upper()
+            new_expectations.append(TestExpectationParser._inverted_expectation_tokens.get(expectation, expectation))
+
+        result = ''
+        if include_modifiers and (bugs or new_modifiers):
+            if bugs:
+                result += ' '.join(bugs) + ' '
+            if new_modifiers:
+                result += '[ %s ] ' % ' '.join(new_modifiers)
+        result += name
+        if include_expectations and new_expectations and set(new_expectations) != set(['Skip', 'Pass']):
+            result += ' [ %s ]' % ' '.join(sorted(set(new_expectations)))
+        if include_comment and comment is not None:
+            result += " #%s" % comment
+        return result
+
+
+# FIXME: Refactor API to be a proper CRUD.
+class TestExpectationsModel(object):
+    """Represents relational store of all expectations and provides CRUD semantics to manage it."""
+
+    def __init__(self, shorten_filename=None):
+        # Maps a test to its list of expectations.
+        self._test_to_expectations = {}
+
+        # Maps a test to list of its modifiers (string values)
+        self._test_to_modifiers = {}
+
+        # Maps a test to a TestExpectationLine instance.
+        self._test_to_expectation_line = {}
+
+        self._modifier_to_tests = self._dict_of_sets(TestExpectations.MODIFIERS)
+        self._expectation_to_tests = self._dict_of_sets(TestExpectations.EXPECTATIONS)
+        self._timeline_to_tests = self._dict_of_sets(TestExpectations.TIMELINES)
+        self._result_type_to_tests = self._dict_of_sets(TestExpectations.RESULT_TYPES)
+
+        self._shorten_filename = shorten_filename or (lambda x: x)
+
+    def _dict_of_sets(self, strings_to_constants):
+        """Takes a dict of strings->constants and returns a dict mapping
+        each constant to an empty set."""
+        d = {}
+        for c in strings_to_constants.values():
+            d[c] = set()
+        return d
+
+    def get_test_set(self, modifier, expectation=None, include_skips=True):
+        if expectation is None:
+            tests = self._modifier_to_tests[modifier]
+        else:
+            tests = (self._expectation_to_tests[expectation] &
+                self._modifier_to_tests[modifier])
+
+        if not include_skips:
+            tests = tests - self.get_test_set(SKIP, expectation)
+
+        return tests
+
+    def get_test_set_for_keyword(self, keyword):
+        # FIXME: get_test_set() is an awkward public interface because it requires
+        # callers to know the difference between modifiers and expectations. We
+        # should replace that with this where possible.
+        expectation_enum = TestExpectations.EXPECTATIONS.get(keyword.lower(), None)
+        if expectation_enum is not None:
+            return self._expectation_to_tests[expectation_enum]
+        modifier_enum = TestExpectations.MODIFIERS.get(keyword.lower(), None)
+        if modifier_enum is not None:
+            return self._modifier_to_tests[modifier_enum]
+
+        # We must not have an index on this modifier.
+        matching_tests = set()
+        for test, modifiers in self._test_to_modifiers.iteritems():
+            if keyword.lower() in modifiers:
+                matching_tests.add(test)
+        return matching_tests
+
+    def get_tests_with_result_type(self, result_type):
+        return self._result_type_to_tests[result_type]
+
+    def get_tests_with_timeline(self, timeline):
+        return self._timeline_to_tests[timeline]
+
+    def get_modifiers(self, test):
+        """This returns modifiers for the given test (the modifiers plus the BUGXXXX identifier). This is used by the LTTF dashboard."""
+        return self._test_to_modifiers[test]
+
+    def has_modifier(self, test, modifier):
+        return test in self._modifier_to_tests[modifier]
+
+    def has_keyword(self, test, keyword):
+        return (keyword.upper() in self.get_expectations_string(test) or
+                keyword.lower() in self.get_modifiers(test))
+
+    def has_test(self, test):
+        return test in self._test_to_expectation_line
+
+    def get_expectation_line(self, test):
+        return self._test_to_expectation_line.get(test)
+
+    def get_expectations(self, test):
+        return self._test_to_expectations[test]
+
+    def get_expectations_string(self, test):
+        """Returns the expectatons for the given test as an uppercase string.
+        If there are no expectations for the test, then "PASS" is returned."""
+        expectations = self.get_expectations(test)
+        retval = []
+
+        for expectation in expectations:
+            retval.append(self.expectation_to_string(expectation))
+
+        return " ".join(retval)
+
+    def expectation_to_string(self, expectation):
+        """Return the uppercased string equivalent of a given expectation."""
+        for item in TestExpectations.EXPECTATIONS.items():
+            if item[1] == expectation:
+                return item[0].upper()
+        raise ValueError(expectation)
+
+
+    def add_expectation_line(self, expectation_line, in_skipped=False):
+        """Returns a list of warnings encountered while matching modifiers."""
+
+        if expectation_line.is_invalid():
+            return
+
+        for test in expectation_line.matching_tests:
+            if not in_skipped and self._already_seen_better_match(test, expectation_line):
+                continue
+
+            self._clear_expectations_for_test(test)
+            self._test_to_expectation_line[test] = expectation_line
+            self._add_test(test, expectation_line)
+
+    def _add_test(self, test, expectation_line):
+        """Sets the expected state for a given test.
+
+        This routine assumes the test has not been added before. If it has,
+        use _clear_expectations_for_test() to reset the state prior to
+        calling this."""
+        self._test_to_expectations[test] = expectation_line.parsed_expectations
+        for expectation in expectation_line.parsed_expectations:
+            self._expectation_to_tests[expectation].add(test)
+
+        self._test_to_modifiers[test] = expectation_line.modifiers
+        for modifier in expectation_line.parsed_modifiers:
+            mod_value = TestExpectations.MODIFIERS[modifier]
+            self._modifier_to_tests[mod_value].add(test)
+
+        if TestExpectationParser.WONTFIX_MODIFIER in expectation_line.parsed_modifiers:
+            self._timeline_to_tests[WONTFIX].add(test)
+        else:
+            self._timeline_to_tests[NOW].add(test)
+
+        if TestExpectationParser.SKIP_MODIFIER in expectation_line.parsed_modifiers:
+            self._result_type_to_tests[SKIP].add(test)
+        elif expectation_line.parsed_expectations == set([PASS]):
+            self._result_type_to_tests[PASS].add(test)
+        elif expectation_line.is_flaky():
+            self._result_type_to_tests[FLAKY].add(test)
+        else:
+            # FIXME: What is this?
+            self._result_type_to_tests[FAIL].add(test)
+
+    def _clear_expectations_for_test(self, test):
+        """Remove prexisting expectations for this test.
+        This happens if we are seeing a more precise path
+        than a previous listing.
+        """
+        if self.has_test(test):
+            self._test_to_expectations.pop(test, '')
+            self._remove_from_sets(test, self._expectation_to_tests)
+            self._remove_from_sets(test, self._modifier_to_tests)
+            self._remove_from_sets(test, self._timeline_to_tests)
+            self._remove_from_sets(test, self._result_type_to_tests)
+
+    def _remove_from_sets(self, test, dict_of_sets_of_tests):
+        """Removes the given test from the sets in the dictionary.
+
+        Args:
+          test: test to look for
+          dict: dict of sets of files"""
+        for set_of_tests in dict_of_sets_of_tests.itervalues():
+            if test in set_of_tests:
+                set_of_tests.remove(test)
+
+    def _already_seen_better_match(self, test, expectation_line):
+        """Returns whether we've seen a better match already in the file.
+
+        Returns True if we've already seen a expectation_line.name that matches more of the test
+            than this path does
+        """
+        # FIXME: See comment below about matching test configs and specificity.
+        if not self.has_test(test):
+            # We've never seen this test before.
+            return False
+
+        prev_expectation_line = self._test_to_expectation_line[test]
+
+        if prev_expectation_line.filename != expectation_line.filename:
+            # We've moved on to a new expectation file, which overrides older ones.
+            return False
+
+        if len(prev_expectation_line.path) > len(expectation_line.path):
+            # The previous path matched more of the test.
+            return True
+
+        if len(prev_expectation_line.path) < len(expectation_line.path):
+            # This path matches more of the test.
+            return False
+
+        # At this point we know we have seen a previous exact match on this
+        # base path, so we need to check the two sets of modifiers.
+
+        # FIXME: This code was originally designed to allow lines that matched
+        # more modifiers to override lines that matched fewer modifiers.
+        # However, we currently view these as errors.
+        #
+        # To use the "more modifiers wins" policy, change the errors for overrides
+        # to be warnings and return False".
+
+        if prev_expectation_line.matching_configurations == expectation_line.matching_configurations:
+            expectation_line.warnings.append('Duplicate or ambiguous entry lines %s:%d and %s:%d.' % (
+                self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_number,
+                self._shorten_filename(expectation_line.filename), expectation_line.line_number))
+            return True
+
+        if prev_expectation_line.matching_configurations >= expectation_line.matching_configurations:
+            expectation_line.warnings.append('More specific entry for %s on line %s:%d overrides line %s:%d.' % (expectation_line.name,
+                self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_number,
+                self._shorten_filename(expectation_line.filename), expectation_line.line_number))
+            # FIXME: return False if we want more specific to win.
+            return True
+
+        if prev_expectation_line.matching_configurations <= expectation_line.matching_configurations:
+            expectation_line.warnings.append('More specific entry for %s on line %s:%d overrides line %s:%d.' % (expectation_line.name,
+                self._shorten_filename(expectation_line.filename), expectation_line.line_number,
+                self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_number))
+            return True
+
+        if prev_expectation_line.matching_configurations & expectation_line.matching_configurations:
+            expectation_line.warnings.append('Entries for %s on lines %s:%d and %s:%d match overlapping sets of configurations.' % (expectation_line.name,
+                self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_number,
+                self._shorten_filename(expectation_line.filename), expectation_line.line_number))
+            return True
+
+        # Configuration sets are disjoint, then.
+        return False
+
+
+class TestExpectations(object):
+    """Test expectations consist of lines with specifications of what
+    to expect from layout test cases. The test cases can be directories
+    in which case the expectations apply to all test cases in that
+    directory and any subdirectory. The format is along the lines of:
+
+      LayoutTests/fast/js/fixme.js [ Failure ]
+      LayoutTests/fast/js/flaky.js [ Failure Pass ]
+      LayoutTests/fast/js/crash.js [ Crash Failure Pass Timeout ]
+      ...
+
+    To add modifiers:
+      LayoutTests/fast/js/no-good.js
+      [ Debug ] LayoutTests/fast/js/no-good.js [ Pass Timeout ]
+      [ Debug ] LayoutTests/fast/js/no-good.js [ Pass Skip Timeout ]
+      [ Linux Debug ] LayoutTests/fast/js/no-good.js [ Pass Skip Timeout ]
+      [ Linux Win ] LayoutTests/fast/js/no-good.js [ Pass Skip Timeout ]
+
+    Skip: Doesn't run the test.
+    Slow: The test takes a long time to run, but does not timeout indefinitely.
+    WontFix: For tests that we never intend to pass on a given platform (treated like Skip).
+
+    Notes:
+      -A test cannot be both SLOW and TIMEOUT
+      -A test can be included twice, but not via the same path.
+      -If a test is included twice, then the more precise path wins.
+      -CRASH tests cannot be WONTFIX
+    """
+
+    # FIXME: Update to new syntax once the old format is no longer supported.
+    EXPECTATIONS = {'pass': PASS,
+                    'audio': AUDIO,
+                    'fail': FAIL,
+                    'image': IMAGE,
+                    'image+text': IMAGE_PLUS_TEXT,
+                    'text': TEXT,
+                    'timeout': TIMEOUT,
+                    'crash': CRASH,
+                    'missing': MISSING}
+
+    # (aggregated by category, pass/fail/skip, type)
+    EXPECTATION_DESCRIPTIONS = {SKIP: 'skipped',
+                                PASS: 'passes',
+                                FAIL: 'failures',
+                                IMAGE: 'image-only failures',
+                                TEXT: 'text-only failures',
+                                IMAGE_PLUS_TEXT: 'image and text failures',
+                                AUDIO: 'audio failures',
+                                CRASH: 'crashes',
+                                TIMEOUT: 'timeouts',
+                                MISSING: 'missing results'}
+
+    EXPECTATION_ORDER = (PASS, CRASH, TIMEOUT, MISSING, FAIL, IMAGE, SKIP)
+
+    BUILD_TYPES = ('debug', 'release')
+
+    MODIFIERS = {TestExpectationParser.SKIP_MODIFIER: SKIP,
+                 TestExpectationParser.WONTFIX_MODIFIER: WONTFIX,
+                 TestExpectationParser.SLOW_MODIFIER: SLOW,
+                 TestExpectationParser.REBASELINE_MODIFIER: REBASELINE,
+                 'none': NONE}
+
+    TIMELINES = {TestExpectationParser.WONTFIX_MODIFIER: WONTFIX,
+                 'now': NOW}
+
+    RESULT_TYPES = {'skip': SKIP,
+                    'pass': PASS,
+                    'fail': FAIL,
+                    'flaky': FLAKY}
+
+    @classmethod
+    def expectation_from_string(cls, string):
+        assert(' ' not in string)  # This only handles one expectation at a time.
+        return cls.EXPECTATIONS.get(string.lower())
+
+    @staticmethod
+    def result_was_expected(result, expected_results, test_needs_rebaselining, test_is_skipped):
+        """Returns whether we got a result we were expecting.
+        Args:
+            result: actual result of a test execution
+            expected_results: set of results listed in test_expectations
+            test_needs_rebaselining: whether test was marked as REBASELINE
+            test_is_skipped: whether test was marked as SKIP"""
+        if result in expected_results:
+            return True
+        if result in (TEXT, IMAGE_PLUS_TEXT, AUDIO) and (FAIL in expected_results):
+            return True
+        if result == MISSING and test_needs_rebaselining:
+            return True
+        if result == SKIP and test_is_skipped:
+            return True
+        return False
+
+    @staticmethod
+    def remove_pixel_failures(expected_results):
+        """Returns a copy of the expected results for a test, except that we
+        drop any pixel failures and return the remaining expectations. For example,
+        if we're not running pixel tests, then tests expected to fail as IMAGE
+        will PASS."""
+        expected_results = expected_results.copy()
+        if IMAGE in expected_results:
+            expected_results.remove(IMAGE)
+            expected_results.add(PASS)
+        return expected_results
+
+    @staticmethod
+    def has_pixel_failures(actual_results):
+        return IMAGE in actual_results or FAIL in actual_results
+
+    @staticmethod
+    def suffixes_for_expectations(expectations):
+        suffixes = set()
+        if IMAGE in expectations:
+            suffixes.add('png')
+        if FAIL in expectations:
+            suffixes.add('txt')
+            suffixes.add('png')
+            suffixes.add('wav')
+        return set(suffixes)
+
+    # FIXME: This constructor does too much work. We should move the actual parsing of
+    # the expectations into separate routines so that linting and handling overrides
+    # can be controlled separately, and the constructor can be more of a no-op.
+    def __init__(self, port, tests=None, include_overrides=True, expectations_to_lint=None):
+        self._full_test_list = tests
+        self._test_config = port.test_configuration()
+        self._is_lint_mode = expectations_to_lint is not None
+        self._model = TestExpectationsModel(self._shorten_filename)
+        self._parser = TestExpectationParser(port, tests, self._is_lint_mode)
+        self._port = port
+        self._skipped_tests_warnings = []
+
+        expectations_dict = expectations_to_lint or port.expectations_dict()
+        self._expectations = self._parser.parse(expectations_dict.keys()[0], expectations_dict.values()[0])
+        self._add_expectations(self._expectations)
+
+        if len(expectations_dict) > 1 and include_overrides:
+            for name in expectations_dict.keys()[1:]:
+                expectations = self._parser.parse(name, expectations_dict[name])
+                self._add_expectations(expectations)
+                self._expectations += expectations
+
+        # FIXME: move ignore_tests into port.skipped_layout_tests()
+        self.add_skipped_tests(port.skipped_layout_tests(tests).union(set(port.get_option('ignore_tests', []))))
+
+        self._has_warnings = False
+        self._report_warnings()
+        self._process_tests_without_expectations()
+
+    # TODO(ojan): Allow for removing skipped tests when getting the list of
+    # tests to run, but not when getting metrics.
+    def model(self):
+        return self._model
+
+    def get_rebaselining_failures(self):
+        return self._model.get_test_set(REBASELINE)
+
+    # FIXME: Change the callsites to use TestExpectationsModel and remove.
+    def get_expectations(self, test):
+        return self._model.get_expectations(test)
+
+    # FIXME: Change the callsites to use TestExpectationsModel and remove.
+    def has_modifier(self, test, modifier):
+        return self._model.has_modifier(test, modifier)
+
+    # FIXME: Change the callsites to use TestExpectationsModel and remove.
+    def get_tests_with_result_type(self, result_type):
+        return self._model.get_tests_with_result_type(result_type)
+
+    # FIXME: Change the callsites to use TestExpectationsModel and remove.
+    def get_test_set(self, modifier, expectation=None, include_skips=True):
+        return self._model.get_test_set(modifier, expectation, include_skips)
+
+    # FIXME: Change the callsites to use TestExpectationsModel and remove.
+    def get_modifiers(self, test):
+        return self._model.get_modifiers(test)
+
+    # FIXME: Change the callsites to use TestExpectationsModel and remove.
+    def get_tests_with_timeline(self, timeline):
+        return self._model.get_tests_with_timeline(timeline)
+
+    def get_expectations_string(self, test):
+        return self._model.get_expectations_string(test)
+
+    def expectation_to_string(self, expectation):
+        return self._model.expectation_to_string(expectation)
+
+    def matches_an_expected_result(self, test, result, pixel_tests_are_enabled):
+        expected_results = self._model.get_expectations(test)
+        if not pixel_tests_are_enabled:
+            expected_results = self.remove_pixel_failures(expected_results)
+        return self.result_was_expected(result,
+                                   expected_results,
+                                   self.is_rebaselining(test),
+                                   self._model.has_modifier(test, SKIP))
+
+    def is_rebaselining(self, test):
+        return self._model.has_modifier(test, REBASELINE)
+
+    def _shorten_filename(self, filename):
+        if filename.startswith(self._port.path_from_webkit_base()):
+            return self._port.host.filesystem.relpath(filename, self._port.path_from_webkit_base())
+        return filename
+
+    def _report_warnings(self):
+        warnings = []
+        for expectation in self._expectations:
+            for warning in expectation.warnings:
+                warnings.append('%s:%d %s %s' % (self._shorten_filename(expectation.filename), expectation.line_number,
+                                warning, expectation.name if expectation.expectations else expectation.original_string))
+
+        if warnings:
+            self._has_warnings = True
+            if self._is_lint_mode:
+                raise ParseError(warnings)
+            _log.warning('--lint-test-files warnings:')
+            for warning in warnings:
+                _log.warning(warning)
+            _log.warning('')
+
+    def _process_tests_without_expectations(self):
+        if self._full_test_list:
+            for test in self._full_test_list:
+                if not self._model.has_test(test):
+                    self._model.add_expectation_line(TestExpectationLine.create_passing_expectation(test))
+
+    def has_warnings(self):
+        return self._has_warnings
+
+    def remove_configuration_from_test(self, test, test_configuration):
+        expectations_to_remove = []
+        modified_expectations = []
+
+        for expectation in self._expectations:
+            if expectation.name != test or expectation.is_flaky() or not expectation.parsed_expectations:
+                continue
+            if iter(expectation.parsed_expectations).next() not in (FAIL, IMAGE):
+                continue
+            if test_configuration not in expectation.matching_configurations:
+                continue
+
+            expectation.matching_configurations.remove(test_configuration)
+            if expectation.matching_configurations:
+                modified_expectations.append(expectation)
+            else:
+                expectations_to_remove.append(expectation)
+
+        for expectation in expectations_to_remove:
+            self._expectations.remove(expectation)
+
+        return self.list_to_string(self._expectations, self._parser._test_configuration_converter, modified_expectations)
+
+    def remove_rebaselined_tests(self, except_these_tests, filename):
+        """Returns a copy of the expectations in the file with the tests removed."""
+        def without_rebaseline_modifier(expectation):
+            return (expectation.filename == filename and
+                    not (not expectation.is_invalid() and
+                         expectation.name in except_these_tests and
+                         'rebaseline' in expectation.parsed_modifiers))
+
+        return self.list_to_string(filter(without_rebaseline_modifier, self._expectations), reconstitute_only_these=[])
+
+    def _add_expectations(self, expectation_list):
+        for expectation_line in expectation_list:
+            if not expectation_line.expectations:
+                continue
+
+            if self._is_lint_mode or self._test_config in expectation_line.matching_configurations:
+                self._model.add_expectation_line(expectation_line)
+
+    def add_skipped_tests(self, tests_to_skip):
+        if not tests_to_skip:
+            return
+        for test in self._expectations:
+            if test.name and test.name in tests_to_skip:
+                test.warnings.append('%s:%d %s is also in a Skipped file.' % (test.filename, test.line_number, test.name))
+
+        for test_name in tests_to_skip:
+            expectation_line = self._parser.expectation_for_skipped_test(test_name)
+            self._model.add_expectation_line(expectation_line, in_skipped=True)
+
+    @staticmethod
+    def list_to_string(expectation_lines, test_configuration_converter=None, reconstitute_only_these=None):
+        def serialize(expectation_line):
+            # If reconstitute_only_these is an empty list, we want to return original_string.
+            # So we need to compare reconstitute_only_these to None, not just check if it's falsey.
+            if reconstitute_only_these is None or expectation_line in reconstitute_only_these:
+                return expectation_line.to_string(test_configuration_converter)
+            return expectation_line.original_string
+
+        def nones_out(expectation_line):
+            return expectation_line is not None
+
+        return "\n".join(filter(nones_out, map(serialize, expectation_lines)))
diff --git a/Tools/Scripts/webkitpy/layout_tests/models/test_expectations_unittest.py b/Tools/Scripts/webkitpy/layout_tests/models/test_expectations_unittest.py
new file mode 100644
index 0000000..d78ae3f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/models/test_expectations_unittest.py
@@ -0,0 +1,710 @@
+#!/usr/bin/python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.common.host_mock import MockHost
+from webkitpy.common.system.outputcapture import OutputCapture
+
+from webkitpy.layout_tests.models.test_configuration import *
+from webkitpy.layout_tests.models.test_expectations import *
+from webkitpy.layout_tests.models.test_configuration import *
+
+try:
+    from collections import OrderedDict
+except ImportError:
+    # Needed for Python < 2.7
+    from webkitpy.thirdparty.ordered_dict import OrderedDict
+
+
+class Base(unittest.TestCase):
+    # Note that all of these tests are written assuming the configuration
+    # being tested is Windows XP, Release build.
+
+    def __init__(self, testFunc):
+        host = MockHost()
+        self._port = host.port_factory.get('test-win-xp', None)
+        self._exp = None
+        unittest.TestCase.__init__(self, testFunc)
+
+    def get_test(self, test_name):
+        # FIXME: Remove this routine and just reference test names directly.
+        return test_name
+
+    def get_basic_tests(self):
+        return [self.get_test('failures/expected/text.html'),
+                self.get_test('failures/expected/image_checksum.html'),
+                self.get_test('failures/expected/crash.html'),
+                self.get_test('failures/expected/missing_text.html'),
+                self.get_test('failures/expected/image.html'),
+                self.get_test('passes/text.html')]
+
+    def get_basic_expectations(self):
+        return """
+Bug(test) failures/expected/text.html [ Failure ]
+Bug(test) failures/expected/crash.html [ WontFix ]
+Bug(test) failures/expected/missing_image.html [ Rebaseline Missing ]
+Bug(test) failures/expected/image_checksum.html [ WontFix ]
+Bug(test) failures/expected/image.html [ WontFix Mac ]
+"""
+
+    def parse_exp(self, expectations, overrides=None, is_lint_mode=False):
+        expectations_dict = OrderedDict()
+        expectations_dict['expectations'] = expectations
+        if overrides:
+            expectations_dict['overrides'] = overrides
+        self._port.expectations_dict = lambda: expectations_dict
+        expectations_to_lint = expectations_dict if is_lint_mode else None
+        self._exp = TestExpectations(self._port, self.get_basic_tests(), expectations_to_lint=expectations_to_lint)
+
+    def assert_exp(self, test, result):
+        self.assertEquals(self._exp.get_expectations(self.get_test(test)),
+                          set([result]))
+
+    def assert_bad_expectations(self, expectations, overrides=None):
+        self.assertRaises(ParseError, self.parse_exp, expectations, is_lint_mode=True, overrides=overrides)
+
+
+class BasicTests(Base):
+    def test_basic(self):
+        self.parse_exp(self.get_basic_expectations())
+        self.assert_exp('failures/expected/text.html', FAIL)
+        self.assert_exp('failures/expected/image_checksum.html', PASS)
+        self.assert_exp('passes/text.html', PASS)
+        self.assert_exp('failures/expected/image.html', PASS)
+
+
+class MiscTests(Base):
+    def test_multiple_results(self):
+        self.parse_exp('Bug(x) failures/expected/text.html [ Crash Failure ]')
+        self.assertEqual(self._exp.get_expectations(
+            self.get_test('failures/expected/text.html')),
+            set([FAIL, CRASH]))
+
+    def test_result_was_expected(self):
+        # test basics
+        self.assertEquals(TestExpectations.result_was_expected(PASS, set([PASS]), test_needs_rebaselining=False, test_is_skipped=False), True)
+        self.assertEquals(TestExpectations.result_was_expected(FAIL, set([PASS]), test_needs_rebaselining=False, test_is_skipped=False), False)
+
+        # test handling of SKIPped tests and results
+        self.assertEquals(TestExpectations.result_was_expected(SKIP, set([CRASH]), test_needs_rebaselining=False, test_is_skipped=True), True)
+        self.assertEquals(TestExpectations.result_was_expected(SKIP, set([CRASH]), test_needs_rebaselining=False, test_is_skipped=False), False)
+
+        # test handling of MISSING results and the REBASELINE modifier
+        self.assertEquals(TestExpectations.result_was_expected(MISSING, set([PASS]), test_needs_rebaselining=True, test_is_skipped=False), True)
+        self.assertEquals(TestExpectations.result_was_expected(MISSING, set([PASS]), test_needs_rebaselining=False, test_is_skipped=False), False)
+
+    def test_remove_pixel_failures(self):
+        self.assertEquals(TestExpectations.remove_pixel_failures(set([FAIL])), set([FAIL]))
+        self.assertEquals(TestExpectations.remove_pixel_failures(set([PASS])), set([PASS]))
+        self.assertEquals(TestExpectations.remove_pixel_failures(set([IMAGE])), set([PASS]))
+        self.assertEquals(TestExpectations.remove_pixel_failures(set([FAIL])), set([FAIL]))
+        self.assertEquals(TestExpectations.remove_pixel_failures(set([PASS, IMAGE, CRASH])), set([PASS, CRASH]))
+
+    def test_suffixes_for_expectations(self):
+        self.assertEquals(TestExpectations.suffixes_for_expectations(set([FAIL])), set(['txt', 'png', 'wav']))
+        self.assertEquals(TestExpectations.suffixes_for_expectations(set([IMAGE])), set(['png']))
+        self.assertEquals(TestExpectations.suffixes_for_expectations(set([FAIL, IMAGE, CRASH])), set(['txt', 'png', 'wav']))
+        self.assertEquals(TestExpectations.suffixes_for_expectations(set()), set())
+
+    def test_category_expectations(self):
+        # This test checks unknown tests are not present in the
+        # expectations and that known test part of a test category is
+        # present in the expectations.
+        exp_str = 'Bug(x) failures/expected [ WontFix ]'
+        self.parse_exp(exp_str)
+        test_name = 'failures/expected/unknown-test.html'
+        unknown_test = self.get_test(test_name)
+        self.assertRaises(KeyError, self._exp.get_expectations,
+                          unknown_test)
+        self.assert_exp('failures/expected/crash.html', PASS)
+
+    def test_get_modifiers(self):
+        self.parse_exp(self.get_basic_expectations())
+        self.assertEqual(self._exp.get_modifiers(
+                         self.get_test('passes/text.html')), [])
+
+    def test_get_expectations_string(self):
+        self.parse_exp(self.get_basic_expectations())
+        self.assertEquals(self._exp.get_expectations_string(
+                          self.get_test('failures/expected/text.html')),
+                          'FAIL')
+
+    def test_expectation_to_string(self):
+        # Normal cases are handled by other tests.
+        self.parse_exp(self.get_basic_expectations())
+        self.assertRaises(ValueError, self._exp.expectation_to_string,
+                          -1)
+
+    def test_get_test_set(self):
+        # Handle some corner cases for this routine not covered by other tests.
+        self.parse_exp(self.get_basic_expectations())
+        s = self._exp.get_test_set(WONTFIX)
+        self.assertEqual(s,
+            set([self.get_test('failures/expected/crash.html'),
+                 self.get_test('failures/expected/image_checksum.html')]))
+
+    def test_parse_warning(self):
+        try:
+            filesystem = self._port.host.filesystem
+            filesystem.write_text_file(filesystem.join(self._port.layout_tests_dir(), 'disabled-test.html-disabled'), 'content')
+            self.get_test('disabled-test.html-disabled'),
+            self.parse_exp("[ FOO ] failures/expected/text.html [ Failure ]\n"
+                "Bug(rniwa) non-existent-test.html [ Failure ]\n"
+                "Bug(rniwa) disabled-test.html-disabled [ ImageOnlyFailure ]", is_lint_mode=True)
+            self.assertFalse(True, "ParseError wasn't raised")
+        except ParseError, e:
+            warnings = ("expectations:1 Unrecognized modifier 'foo' failures/expected/text.html\n"
+                        "expectations:2 Path does not exist. non-existent-test.html")
+            self.assertEqual(str(e), warnings)
+
+    def test_parse_warnings_are_logged_if_not_in_lint_mode(self):
+        oc = OutputCapture()
+        try:
+            oc.capture_output()
+            self.parse_exp('-- this should be a syntax error', is_lint_mode=False)
+        finally:
+            _, _, logs = oc.restore_output()
+            self.assertNotEquals(logs, '')
+
+    def test_error_on_different_platform(self):
+        # parse_exp uses a Windows port. Assert errors on Mac show up in lint mode.
+        self.assertRaises(ParseError, self.parse_exp,
+            'Bug(test) [ Mac ] failures/expected/text.html [ Failure ]\nBug(test) [ Mac ] failures/expected/text.html [ Failure ]',
+            is_lint_mode=True)
+
+    def test_error_on_different_build_type(self):
+        # parse_exp uses a Release port. Assert errors on DEBUG show up in lint mode.
+        self.assertRaises(ParseError, self.parse_exp,
+            'Bug(test) [ Debug ] failures/expected/text.html [ Failure ]\nBug(test) [ Debug ] failures/expected/text.html [ Failure ]',
+            is_lint_mode=True)
+
+    def test_overrides(self):
+        self.parse_exp("Bug(exp) failures/expected/text.html [ Failure ]",
+                       "Bug(override) failures/expected/text.html [ ImageOnlyFailure ]")
+        self.assert_exp('failures/expected/text.html', IMAGE)
+
+    def test_overrides__directory(self):
+        self.parse_exp("Bug(exp) failures/expected/text.html [ Failure ]",
+                       "Bug(override) failures/expected [ Crash ]")
+        self.assert_exp('failures/expected/text.html', CRASH)
+        self.assert_exp('failures/expected/image.html', CRASH)
+
+    def test_overrides__duplicate(self):
+        self.assert_bad_expectations("Bug(exp) failures/expected/text.html [ Failure ]",
+                                     "Bug(override) failures/expected/text.html [ ImageOnlyFailure ]\n"
+                                     "Bug(override) failures/expected/text.html [ Crash ]\n")
+
+    def test_pixel_tests_flag(self):
+        def match(test, result, pixel_tests_enabled):
+            return self._exp.matches_an_expected_result(
+                self.get_test(test), result, pixel_tests_enabled)
+
+        self.parse_exp(self.get_basic_expectations())
+        self.assertTrue(match('failures/expected/text.html', FAIL, True))
+        self.assertTrue(match('failures/expected/text.html', FAIL, False))
+        self.assertFalse(match('failures/expected/text.html', CRASH, True))
+        self.assertFalse(match('failures/expected/text.html', CRASH, False))
+        self.assertTrue(match('failures/expected/image_checksum.html', PASS,
+                              True))
+        self.assertTrue(match('failures/expected/image_checksum.html', PASS,
+                              False))
+        self.assertTrue(match('failures/expected/crash.html', PASS, False))
+        self.assertTrue(match('passes/text.html', PASS, False))
+
+    def test_more_specific_override_resets_skip(self):
+        self.parse_exp("Bug(x) failures/expected [ Skip ]\n"
+                       "Bug(x) failures/expected/text.html [ ImageOnlyFailure ]\n")
+        self.assert_exp('failures/expected/text.html', IMAGE)
+        self.assertFalse(self._port._filesystem.join(self._port.layout_tests_dir(),
+                                                     'failures/expected/text.html') in
+                         self._exp.get_tests_with_result_type(SKIP))
+
+
+class SkippedTests(Base):
+    def check(self, expectations, overrides, skips, lint=False):
+        port = MockHost().port_factory.get('qt')
+        port._filesystem.write_text_file(port._filesystem.join(port.layout_tests_dir(), 'failures/expected/text.html'), 'foo')
+        expectations_dict = OrderedDict()
+        expectations_dict['expectations'] = expectations
+        if overrides:
+            expectations_dict['overrides'] = overrides
+        port.expectations_dict = lambda: expectations_dict
+        port.skipped_layout_tests = lambda tests: set(skips)
+        expectations_to_lint = expectations_dict if lint else None
+        exp = TestExpectations(port, ['failures/expected/text.html'], expectations_to_lint=expectations_to_lint)
+
+        # Check that the expectation is for BUG_DUMMY SKIP : ... [ Pass ]
+        self.assertEquals(exp.get_modifiers('failures/expected/text.html'),
+                          [TestExpectationParser.DUMMY_BUG_MODIFIER, TestExpectationParser.SKIP_MODIFIER, TestExpectationParser.WONTFIX_MODIFIER])
+        self.assertEquals(exp.get_expectations('failures/expected/text.html'), set([PASS]))
+
+    def test_skipped_tests_work(self):
+        self.check(expectations='', overrides=None, skips=['failures/expected/text.html'])
+
+    def test_duplicate_skipped_test_fails_lint(self):
+        self.assertRaises(ParseError, self.check, expectations='Bug(x) failures/expected/text.html [ Failure ]\n', overrides=None, skips=['failures/expected/text.html'], lint=True)
+
+    def test_skipped_file_overrides_expectations(self):
+        self.check(expectations='Bug(x) failures/expected/text.html [ Failure ]\n', overrides=None,
+                   skips=['failures/expected/text.html'])
+
+    def test_skipped_dir_overrides_expectations(self):
+        self.check(expectations='Bug(x) failures/expected/text.html [ Failure ]\n', overrides=None,
+                   skips=['failures/expected'])
+
+    def test_skipped_file_overrides_overrides(self):
+        self.check(expectations='', overrides='Bug(x) failures/expected/text.html [ Failure ]\n',
+                   skips=['failures/expected/text.html'])
+
+    def test_skipped_dir_overrides_overrides(self):
+        self.check(expectations='', overrides='Bug(x) failures/expected/text.html [ Failure ]\n',
+                   skips=['failures/expected'])
+
+    def test_skipped_entry_dont_exist(self):
+        port = MockHost().port_factory.get('qt')
+        expectations_dict = OrderedDict()
+        expectations_dict['expectations'] = ''
+        port.expectations_dict = lambda: expectations_dict
+        port.skipped_layout_tests = lambda tests: set(['foo/bar/baz.html'])
+        capture = OutputCapture()
+        capture.capture_output()
+        exp = TestExpectations(port)
+        _, _, logs = capture.restore_output()
+        self.assertEqual('The following test foo/bar/baz.html from the Skipped list doesn\'t exist\n', logs)
+
+
+class ExpectationSyntaxTests(Base):
+    def test_unrecognized_expectation(self):
+        self.assert_bad_expectations('Bug(test) failures/expected/text.html [ Unknown ]')
+
+    def test_macro(self):
+        exp_str = 'Bug(test) [ Win ] failures/expected/text.html [ Failure ]'
+        self.parse_exp(exp_str)
+        self.assert_exp('failures/expected/text.html', FAIL)
+
+    def assert_tokenize_exp(self, line, bugs=None, modifiers=None, expectations=None, warnings=None, comment=None, name='foo.html'):
+        bugs = bugs or []
+        modifiers = modifiers or []
+        expectations = expectations or []
+        warnings = warnings or []
+        filename = 'TestExpectations'
+        line_number = 1
+        expectation_line = TestExpectationParser._tokenize_line(filename, line, line_number)
+        self.assertEquals(expectation_line.warnings, warnings)
+        self.assertEquals(expectation_line.name, name)
+        self.assertEquals(expectation_line.filename, filename)
+        self.assertEquals(expectation_line.line_number, line_number)
+        if not warnings:
+            self.assertEquals(expectation_line.modifiers, modifiers)
+            self.assertEquals(expectation_line.expectations, expectations)
+
+    def test_bare_name(self):
+        self.assert_tokenize_exp('foo.html', modifiers=['SKIP'], expectations=['PASS'])
+
+    def test_bare_name_and_bugs(self):
+        self.assert_tokenize_exp('webkit.org/b/12345 foo.html', modifiers=['BUGWK12345', 'SKIP'], expectations=['PASS'])
+        self.assert_tokenize_exp('crbug.com/12345 foo.html', modifiers=['BUGCR12345', 'SKIP'], expectations=['PASS'])
+        self.assert_tokenize_exp('Bug(dpranke) foo.html', modifiers=['BUGDPRANKE', 'SKIP'], expectations=['PASS'])
+        self.assert_tokenize_exp('crbug.com/12345 crbug.com/34567 foo.html', modifiers=['BUGCR12345', 'BUGCR34567', 'SKIP'], expectations=['PASS'])
+
+    def test_comments(self):
+        self.assert_tokenize_exp("# comment", name=None, comment="# comment")
+        self.assert_tokenize_exp("foo.html # comment", comment="# comment", expectations=['PASS'], modifiers=['SKIP'])
+
+    def test_config_modifiers(self):
+        self.assert_tokenize_exp('[ Mac ] foo.html', modifiers=['MAC', 'SKIP'], expectations=['PASS'])
+        self.assert_tokenize_exp('[ Mac Vista ] foo.html', modifiers=['MAC', 'VISTA', 'SKIP'], expectations=['PASS'])
+        self.assert_tokenize_exp('[ Mac ] foo.html [ Failure ] ', modifiers=['MAC'], expectations=['FAIL'])
+
+    def test_unknown_config(self):
+        self.assert_tokenize_exp('[ Foo ] foo.html ', modifiers=['Foo', 'SKIP'], expectations=['PASS'])
+
+    def test_unknown_expectation(self):
+        self.assert_tokenize_exp('foo.html [ Audio ]', expectations=['Audio'])
+
+    def test_skip(self):
+        self.assert_tokenize_exp('foo.html [ Skip ]', modifiers=['SKIP'], expectations=['PASS'])
+
+    def test_slow(self):
+        self.assert_tokenize_exp('foo.html [ Slow ]', modifiers=['SLOW'], expectations=['PASS'])
+
+    def test_wontfix(self):
+        self.assert_tokenize_exp('foo.html [ WontFix ]', modifiers=['WONTFIX', 'SKIP'], expectations=['PASS'])
+
+    def test_blank_line(self):
+        self.assert_tokenize_exp('', name=None)
+
+    def test_warnings(self):
+        self.assert_tokenize_exp('[ Mac ]', warnings=['Did not find a test name.'], name=None)
+        self.assert_tokenize_exp('[ [', warnings=['unexpected "["'], name=None)
+        self.assert_tokenize_exp('crbug.com/12345 ]', warnings=['unexpected "]"'], name=None)
+
+        self.assert_tokenize_exp('foo.html crbug.com/12345 ]', warnings=['"crbug.com/12345" is not at the start of the line.'])
+
+
+class SemanticTests(Base):
+    def test_bug_format(self):
+        self.assertRaises(ParseError, self.parse_exp, 'BUG1234 failures/expected/text.html [ Failure ]', is_lint_mode=True)
+
+    def test_bad_bugid(self):
+        try:
+            self.parse_exp('BUG1234 failures/expected/text.html [ Failure ]', is_lint_mode=True)
+            self.fail('should have raised an error about a bad bug identifier')
+        except ParseError, exp:
+            self.assertEquals(len(exp.warnings), 1)
+
+    def test_missing_bugid(self):
+        self.parse_exp('failures/expected/text.html [ Failure ]')
+        self.assertFalse(self._exp.has_warnings())
+
+        self._port.warn_if_bug_missing_in_test_expectations = lambda: True
+
+        self.parse_exp('failures/expected/text.html [ Failure ]')
+        line = self._exp._model.get_expectation_line('failures/expected/text.html')
+        self.assertFalse(line.is_invalid())
+        self.assertEquals(line.warnings, ['Test lacks BUG modifier.'])
+
+    def test_skip_and_wontfix(self):
+        # Skip and WontFix are not allowed to have other expectations as well, because those
+        # expectations won't be exercised and may become stale .
+        self.parse_exp('failures/expected/text.html [ Failure Skip ]')
+        self.assertTrue(self._exp.has_warnings())
+
+        self.parse_exp('failures/expected/text.html [ Crash WontFix ]')
+        self.assertTrue(self._exp.has_warnings())
+
+        self.parse_exp('failures/expected/text.html [ Pass WontFix ]')
+        self.assertTrue(self._exp.has_warnings())
+
+    def test_slow_and_timeout(self):
+        # A test cannot be SLOW and expected to TIMEOUT.
+        self.assertRaises(ParseError, self.parse_exp,
+            'Bug(test) failures/expected/timeout.html [ Slow Timeout ]', is_lint_mode=True)
+
+    def test_rebaseline(self):
+        # Can't lint a file w/ 'REBASELINE' in it.
+        self.assertRaises(ParseError, self.parse_exp,
+            'Bug(test) failures/expected/text.html [ Failure Rebaseline ]',
+            is_lint_mode=True)
+
+    def test_duplicates(self):
+        self.assertRaises(ParseError, self.parse_exp, """
+Bug(exp) failures/expected/text.html [ Failure ]
+Bug(exp) failures/expected/text.html [ ImageOnlyFailure ]""", is_lint_mode=True)
+
+        self.assertRaises(ParseError, self.parse_exp,
+            self.get_basic_expectations(), overrides="""
+Bug(override) failures/expected/text.html [ Failure ]
+Bug(override) failures/expected/text.html [ ImageOnlyFailure ]""", is_lint_mode=True)
+
+    def test_missing_file(self):
+        self.parse_exp('Bug(test) missing_file.html [ Failure ]')
+        self.assertTrue(self._exp.has_warnings(), 1)
+
+
+class PrecedenceTests(Base):
+    def test_file_over_directory(self):
+        # This tests handling precedence of specific lines over directories
+        # and tests expectations covering entire directories.
+        exp_str = """
+Bug(x) failures/expected/text.html [ Failure ]
+Bug(y) failures/expected [ WontFix ]
+"""
+        self.parse_exp(exp_str)
+        self.assert_exp('failures/expected/text.html', FAIL)
+        self.assert_exp('failures/expected/crash.html', PASS)
+
+        exp_str = """
+Bug(x) failures/expected [ WontFix ]
+Bug(y) failures/expected/text.html [ Failure ]
+"""
+        self.parse_exp(exp_str)
+        self.assert_exp('failures/expected/text.html', FAIL)
+        self.assert_exp('failures/expected/crash.html', PASS)
+
+    def test_ambiguous(self):
+        self.assert_bad_expectations("Bug(test) [ Release ] passes/text.html [ Pass ]\n"
+                                     "Bug(test) [ Win ] passes/text.html [ Failure ]\n")
+
+    def test_more_modifiers(self):
+        self.assert_bad_expectations("Bug(test) [ Release ] passes/text.html [ Pass ]\n"
+                                     "Bug(test) [ Win Release ] passes/text.html [ Failure ]\n")
+
+    def test_order_in_file(self):
+        self.assert_bad_expectations("Bug(test) [ Win Release ] : passes/text.html [ Failure ]\n"
+                                     "Bug(test) [ Release ] : passes/text.html [ Pass ]\n")
+
+    def test_macro_overrides(self):
+        self.assert_bad_expectations("Bug(test) [ Win ] passes/text.html [ Pass ]\n"
+                                     "Bug(test) [ XP ] passes/text.html [ Failure ]\n")
+
+
+class RemoveConfigurationsTest(Base):
+    def test_remove(self):
+        host = MockHost()
+        test_port = host.port_factory.get('test-win-xp', None)
+        test_port.test_exists = lambda test: True
+        test_port.test_isfile = lambda test: True
+
+        test_config = test_port.test_configuration()
+        test_port.expectations_dict = lambda: {"expectations": """Bug(x) [ Linux Win Release ] failures/expected/foo.html [ Failure ]
+Bug(y) [ Win Mac Debug ] failures/expected/foo.html [ Crash ]
+"""}
+        expectations = TestExpectations(test_port, self.get_basic_tests())
+
+        actual_expectations = expectations.remove_configuration_from_test('failures/expected/foo.html', test_config)
+
+        self.assertEqual("""Bug(x) [ Linux Vista Win7 Release ] failures/expected/foo.html [ Failure ]
+Bug(y) [ Win Mac Debug ] failures/expected/foo.html [ Crash ]
+""", actual_expectations)
+
+    def test_remove_line(self):
+        host = MockHost()
+        test_port = host.port_factory.get('test-win-xp', None)
+        test_port.test_exists = lambda test: True
+        test_port.test_isfile = lambda test: True
+
+        test_config = test_port.test_configuration()
+        test_port.expectations_dict = lambda: {'expectations': """Bug(x) [ Win Release ] failures/expected/foo.html [ Failure ]
+Bug(y) [ Win Debug ] failures/expected/foo.html [ Crash ]
+"""}
+        expectations = TestExpectations(test_port)
+
+        actual_expectations = expectations.remove_configuration_from_test('failures/expected/foo.html', test_config)
+        actual_expectations = expectations.remove_configuration_from_test('failures/expected/foo.html', host.port_factory.get('test-win-vista', None).test_configuration())
+        actual_expectations = expectations.remove_configuration_from_test('failures/expected/foo.html', host.port_factory.get('test-win-win7', None).test_configuration())
+
+        self.assertEqual("""Bug(y) [ Win Debug ] failures/expected/foo.html [ Crash ]
+""", actual_expectations)
+
+
+class RebaseliningTest(Base):
+    """Test rebaselining-specific functionality."""
+    def assertRemove(self, input_expectations, input_overrides, tests, expected_expectations, expected_overrides):
+        self.parse_exp(input_expectations, is_lint_mode=False, overrides=input_overrides)
+        actual_expectations = self._exp.remove_rebaselined_tests(tests, 'expectations')
+        self.assertEqual(expected_expectations, actual_expectations)
+        actual_overrides = self._exp.remove_rebaselined_tests(tests, 'overrides')
+        self.assertEqual(expected_overrides, actual_overrides)
+
+    def test_remove(self):
+        self.assertRemove('Bug(x) failures/expected/text.html [ Failure Rebaseline ]\n'
+                          'Bug(y) failures/expected/image.html [ ImageOnlyFailure Rebaseline ]\n'
+                          'Bug(z) failures/expected/crash.html [ Crash ]\n',
+                          'Bug(x0) failures/expected/image.html [ Crash ]\n',
+                          ['failures/expected/text.html'],
+                          'Bug(y) failures/expected/image.html [ ImageOnlyFailure Rebaseline ]\n'
+                          'Bug(z) failures/expected/crash.html [ Crash ]\n',
+                          'Bug(x0) failures/expected/image.html [ Crash ]\n')
+
+        # Ensure that we don't modify unrelated lines, even if we could rewrite them.
+        # i.e., the second line doesn't get rewritten to "Bug(y) failures/expected/skip.html"
+        self.assertRemove('Bug(x) failures/expected/text.html [ Failure Rebaseline ]\n'
+                          'Bug(Y) failures/expected/image.html [ Skip   ]\n'
+                          'Bug(z) failures/expected/crash.html\n',
+                          '',
+                          ['failures/expected/text.html'],
+                          'Bug(Y) failures/expected/image.html [ Skip   ]\n'
+                          'Bug(z) failures/expected/crash.html\n',
+                          '')
+
+    def test_get_rebaselining_failures(self):
+        # Make sure we find a test as needing a rebaseline even if it is not marked as a failure.
+        self.parse_exp('Bug(x) failures/expected/text.html [ Rebaseline ]\n')
+        self.assertEqual(len(self._exp.get_rebaselining_failures()), 1)
+
+        self.parse_exp(self.get_basic_expectations())
+        self.assertEqual(len(self._exp.get_rebaselining_failures()), 0)
+
+
+class TestExpectationSerializationTests(unittest.TestCase):
+    def __init__(self, testFunc):
+        host = MockHost()
+        test_port = host.port_factory.get('test-win-xp', None)
+        self._converter = TestConfigurationConverter(test_port.all_test_configurations(), test_port.configuration_specifier_macros())
+        unittest.TestCase.__init__(self, testFunc)
+
+    def _tokenize(self, line):
+        return TestExpectationParser._tokenize_line('path', line, 0)
+
+    def assert_round_trip(self, in_string, expected_string=None):
+        expectation = self._tokenize(in_string)
+        if expected_string is None:
+            expected_string = in_string
+        self.assertEqual(expected_string, expectation.to_string(self._converter))
+
+    def assert_list_round_trip(self, in_string, expected_string=None):
+        host = MockHost()
+        parser = TestExpectationParser(host.port_factory.get('test-win-xp', None), [], allow_rebaseline_modifier=False)
+        expectations = parser.parse('path', in_string)
+        if expected_string is None:
+            expected_string = in_string
+        self.assertEqual(expected_string, TestExpectations.list_to_string(expectations, self._converter))
+
+    def test_unparsed_to_string(self):
+        expectation = TestExpectationLine()
+
+        self.assertEqual(expectation.to_string(self._converter), '')
+        expectation.comment = ' Qux.'
+        self.assertEqual(expectation.to_string(self._converter), '# Qux.')
+        expectation.name = 'bar'
+        self.assertEqual(expectation.to_string(self._converter), 'bar # Qux.')
+        expectation.modifiers = ['foo']
+        # FIXME: case should be preserved here but we can't until we drop the old syntax.
+        self.assertEqual(expectation.to_string(self._converter), '[ FOO ] bar # Qux.')
+        expectation.expectations = ['bAz']
+        self.assertEqual(expectation.to_string(self._converter), '[ FOO ] bar [ BAZ ] # Qux.')
+        expectation.expectations = ['bAz1', 'baZ2']
+        self.assertEqual(expectation.to_string(self._converter), '[ FOO ] bar [ BAZ1 BAZ2 ] # Qux.')
+        expectation.modifiers = ['foo1', 'foO2']
+        self.assertEqual(expectation.to_string(self._converter), '[ FOO1 FOO2 ] bar [ BAZ1 BAZ2 ] # Qux.')
+        expectation.warnings.append('Oh the horror.')
+        self.assertEqual(expectation.to_string(self._converter), '')
+        expectation.original_string = 'Yes it is!'
+        self.assertEqual(expectation.to_string(self._converter), 'Yes it is!')
+
+    def test_unparsed_list_to_string(self):
+        expectation = TestExpectationLine()
+        expectation.comment = 'Qux.'
+        expectation.name = 'bar'
+        expectation.modifiers = ['foo']
+        expectation.expectations = ['bAz1', 'baZ2']
+        # FIXME: case should be preserved here but we can't until we drop the old syntax.
+        self.assertEqual(TestExpectations.list_to_string([expectation]), '[ FOO ] bar [ BAZ1 BAZ2 ] #Qux.')
+
+    def test_parsed_to_string(self):
+        expectation_line = TestExpectationLine()
+        expectation_line.parsed_bug_modifiers = ['BUGX']
+        expectation_line.name = 'test/name/for/realz.html'
+        expectation_line.parsed_expectations = set([IMAGE])
+        self.assertEqual(expectation_line.to_string(self._converter), None)
+        expectation_line.matching_configurations = set([TestConfiguration('xp', 'x86', 'release')])
+        self.assertEqual(expectation_line.to_string(self._converter), 'Bug(x) [ XP Release ] test/name/for/realz.html [ ImageOnlyFailure ]')
+        expectation_line.matching_configurations = set([TestConfiguration('xp', 'x86', 'release'), TestConfiguration('xp', 'x86', 'debug')])
+        self.assertEqual(expectation_line.to_string(self._converter), 'Bug(x) [ XP ] test/name/for/realz.html [ ImageOnlyFailure ]')
+
+    def test_serialize_parsed_expectations(self):
+        expectation_line = TestExpectationLine()
+        expectation_line.parsed_expectations = set([])
+        parsed_expectation_to_string = dict([[parsed_expectation, expectation_string] for expectation_string, parsed_expectation in TestExpectations.EXPECTATIONS.items()])
+        self.assertEqual(expectation_line._serialize_parsed_expectations(parsed_expectation_to_string), '')
+        expectation_line.parsed_expectations = set([FAIL])
+        self.assertEqual(expectation_line._serialize_parsed_expectations(parsed_expectation_to_string), 'fail')
+        expectation_line.parsed_expectations = set([PASS, IMAGE])
+        self.assertEqual(expectation_line._serialize_parsed_expectations(parsed_expectation_to_string), 'pass image')
+        expectation_line.parsed_expectations = set([FAIL, PASS])
+        self.assertEqual(expectation_line._serialize_parsed_expectations(parsed_expectation_to_string), 'pass fail')
+
+    def test_serialize_parsed_modifier_string(self):
+        expectation_line = TestExpectationLine()
+        expectation_line.parsed_bug_modifiers = ['garden-o-matic']
+        expectation_line.parsed_modifiers = ['for', 'the']
+        self.assertEqual(expectation_line._serialize_parsed_modifiers(self._converter, []), 'garden-o-matic for the')
+        self.assertEqual(expectation_line._serialize_parsed_modifiers(self._converter, ['win']), 'garden-o-matic for the win')
+        expectation_line.parsed_bug_modifiers = []
+        expectation_line.parsed_modifiers = []
+        self.assertEqual(expectation_line._serialize_parsed_modifiers(self._converter, []), '')
+        self.assertEqual(expectation_line._serialize_parsed_modifiers(self._converter, ['win']), 'win')
+        expectation_line.parsed_bug_modifiers = ['garden-o-matic', 'total', 'is']
+        self.assertEqual(expectation_line._serialize_parsed_modifiers(self._converter, ['win']), 'garden-o-matic is total win')
+        expectation_line.parsed_bug_modifiers = []
+        expectation_line.parsed_modifiers = ['garden-o-matic', 'total', 'is']
+        self.assertEqual(expectation_line._serialize_parsed_modifiers(self._converter, ['win']), 'garden-o-matic is total win')
+
+    def test_format_line(self):
+        self.assertEqual(TestExpectationLine._format_line(['MODIFIERS'], 'name', ['EXPECTATIONS'], 'comment'), '[ MODIFIERS ] name [ EXPECTATIONS ] #comment')
+        self.assertEqual(TestExpectationLine._format_line(['MODIFIERS'], 'name', ['EXPECTATIONS'], None), '[ MODIFIERS ] name [ EXPECTATIONS ]')
+
+    def test_string_roundtrip(self):
+        self.assert_round_trip('')
+        self.assert_round_trip('FOO')
+        self.assert_round_trip('[')
+        self.assert_round_trip('FOO [')
+        self.assert_round_trip('FOO ] bar')
+        self.assert_round_trip('  FOO [')
+        self.assert_round_trip('    [ FOO ] ')
+        self.assert_round_trip('[ FOO ] bar [ BAZ ]')
+        self.assert_round_trip('[ FOO ] bar [ BAZ ] # Qux.')
+        self.assert_round_trip('[ FOO ] bar [ BAZ ] # Qux.')
+        self.assert_round_trip('[ FOO ] bar [ BAZ ] # Qux.     ')
+        self.assert_round_trip('[ FOO ] bar [ BAZ ] #        Qux.     ')
+        self.assert_round_trip('[ FOO ] ] ] bar BAZ')
+        self.assert_round_trip('[ FOO ] ] ] bar [ BAZ ]')
+        self.assert_round_trip('FOO ] ] bar ==== BAZ')
+        self.assert_round_trip('=')
+        self.assert_round_trip('#')
+        self.assert_round_trip('# ')
+        self.assert_round_trip('# Foo')
+        self.assert_round_trip('# Foo')
+        self.assert_round_trip('# Foo :')
+        self.assert_round_trip('# Foo : =')
+
+    def test_list_roundtrip(self):
+        self.assert_list_round_trip('')
+        self.assert_list_round_trip('\n')
+        self.assert_list_round_trip('\n\n')
+        self.assert_list_round_trip('bar')
+        self.assert_list_round_trip('bar\n# Qux.')
+        self.assert_list_round_trip('bar\n# Qux.\n')
+
+    def test_reconstitute_only_these(self):
+        lines = []
+        reconstitute_only_these = []
+
+        def add_line(matching_configurations, reconstitute):
+            expectation_line = TestExpectationLine()
+            expectation_line.original_string = "Nay"
+            expectation_line.parsed_bug_modifiers = ['BUGX']
+            expectation_line.name = 'Yay'
+            expectation_line.parsed_expectations = set([IMAGE])
+            expectation_line.matching_configurations = matching_configurations
+            lines.append(expectation_line)
+            if reconstitute:
+                reconstitute_only_these.append(expectation_line)
+
+        add_line(set([TestConfiguration('xp', 'x86', 'release')]), True)
+        add_line(set([TestConfiguration('xp', 'x86', 'release'), TestConfiguration('xp', 'x86', 'debug')]), False)
+        serialized = TestExpectations.list_to_string(lines, self._converter)
+        self.assertEquals(serialized, "Bug(x) [ XP Release ] Yay [ ImageOnlyFailure ]\nBug(x) [ XP ] Yay [ ImageOnlyFailure ]")
+        serialized = TestExpectations.list_to_string(lines, self._converter, reconstitute_only_these=reconstitute_only_these)
+        self.assertEquals(serialized, "Bug(x) [ XP Release ] Yay [ ImageOnlyFailure ]\nNay")
+
+    def test_string_whitespace_stripping(self):
+        self.assert_round_trip('\n', '')
+        self.assert_round_trip('  [ FOO ] bar [ BAZ ]', '[ FOO ] bar [ BAZ ]')
+        self.assert_round_trip('[ FOO ]    bar [ BAZ ]', '[ FOO ] bar [ BAZ ]')
+        self.assert_round_trip('[ FOO ] bar [ BAZ ]       # Qux.', '[ FOO ] bar [ BAZ ] # Qux.')
+        self.assert_round_trip('[ FOO ] bar [        BAZ ]  # Qux.', '[ FOO ] bar [ BAZ ] # Qux.')
+        self.assert_round_trip('[ FOO ]       bar [    BAZ ]  # Qux.', '[ FOO ] bar [ BAZ ] # Qux.')
+        self.assert_round_trip('[ FOO ]       bar     [    BAZ ]  # Qux.', '[ FOO ] bar [ BAZ ] # Qux.')
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/layout_tests/models/test_failures.py b/Tools/Scripts/webkitpy/layout_tests/models/test_failures.py
new file mode 100644
index 0000000..402b30a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/models/test_failures.py
@@ -0,0 +1,224 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import cPickle
+
+from webkitpy.layout_tests.models import test_expectations
+
+
+def is_reftest_failure(failure_list):
+    failure_types = [type(f) for f in failure_list]
+    return set((FailureReftestMismatch, FailureReftestMismatchDidNotOccur, FailureReftestNoImagesGenerated)).intersection(failure_types)
+
+# FIXME: This is backwards.  Each TestFailure subclass should know what
+# test_expectation type it corresponds too.  Then this method just
+# collects them all from the failure list and returns the worst one.
+def determine_result_type(failure_list):
+    """Takes a set of test_failures and returns which result type best fits
+    the list of failures. "Best fits" means we use the worst type of failure.
+
+    Returns:
+      one of the test_expectations result types - PASS, FAIL, CRASH, etc."""
+
+    if not failure_list or len(failure_list) == 0:
+        return test_expectations.PASS
+
+    failure_types = [type(f) for f in failure_list]
+    if FailureCrash in failure_types:
+        return test_expectations.CRASH
+    elif FailureTimeout in failure_types:
+        return test_expectations.TIMEOUT
+    elif FailureEarlyExit in failure_types:
+        return test_expectations.SKIP
+    elif (FailureMissingResult in failure_types or
+          FailureMissingImage in failure_types or
+          FailureMissingImageHash in failure_types or
+          FailureMissingAudio in failure_types):
+        return test_expectations.MISSING
+    else:
+        is_text_failure = FailureTextMismatch in failure_types
+        is_image_failure = (FailureImageHashIncorrect in failure_types or
+                            FailureImageHashMismatch in failure_types)
+        is_audio_failure = (FailureAudioMismatch in failure_types)
+        if is_text_failure and is_image_failure:
+            return test_expectations.IMAGE_PLUS_TEXT
+        elif is_text_failure:
+            return test_expectations.TEXT
+        elif is_image_failure or is_reftest_failure(failure_list):
+            return test_expectations.IMAGE
+        elif is_audio_failure:
+            return test_expectations.AUDIO
+        else:
+            raise ValueError("unclassifiable set of failures: "
+                             + str(failure_types))
+
+
+class TestFailure(object):
+    """Abstract base class that defines the failure interface."""
+
+    @staticmethod
+    def loads(s):
+        """Creates a TestFailure object from the specified string."""
+        return cPickle.loads(s)
+
+    def message(self):
+        """Returns a string describing the failure in more detail."""
+        raise NotImplementedError
+
+    def __eq__(self, other):
+        return self.__class__.__name__ == other.__class__.__name__
+
+    def __ne__(self, other):
+        return self.__class__.__name__ != other.__class__.__name__
+
+    def __hash__(self):
+        return hash(self.__class__.__name__)
+
+    def dumps(self):
+        """Returns the string/JSON representation of a TestFailure."""
+        return cPickle.dumps(self)
+
+    def driver_needs_restart(self):
+        """Returns True if we should kill DumpRenderTree/WebKitTestRunner before the next test."""
+        return False
+
+
+class FailureTimeout(TestFailure):
+    def __init__(self, is_reftest=False):
+        super(FailureTimeout, self).__init__()
+        self.is_reftest = is_reftest
+
+    def message(self):
+        return "test timed out"
+
+    def driver_needs_restart(self):
+        return True
+
+
+class FailureCrash(TestFailure):
+    def __init__(self, is_reftest=False, process_name='DumpRenderTree', pid=None):
+        super(FailureCrash, self).__init__()
+        self.process_name = process_name
+        self.pid = pid
+        self.is_reftest = is_reftest
+
+    def message(self):
+        if self.pid:
+            return "%s crashed [pid=%d]" % (self.process_name, self.pid)
+        return self.process_name + " crashed"
+
+    def driver_needs_restart(self):
+        return True
+
+
+class FailureMissingResult(TestFailure):
+    def message(self):
+        return "-expected.txt was missing"
+
+
+class FailureTextMismatch(TestFailure):
+    def message(self):
+        return "text diff"
+
+class FailureMissingImageHash(TestFailure):
+    def message(self):
+        return "-expected.png was missing an embedded checksum"
+
+
+class FailureMissingImage(TestFailure):
+    def message(self):
+        return "-expected.png was missing"
+
+
+class FailureImageHashMismatch(TestFailure):
+    def __init__(self, diff_percent=0):
+        super(FailureImageHashMismatch, self).__init__()
+        self.diff_percent = diff_percent
+
+    def message(self):
+        return "image diff"
+
+
+class FailureImageHashIncorrect(TestFailure):
+    def message(self):
+        return "-expected.png embedded checksum is incorrect"
+
+
+class FailureReftestMismatch(TestFailure):
+    def __init__(self, reference_filename=None):
+        super(FailureReftestMismatch, self).__init__()
+        self.reference_filename = reference_filename
+        self.diff_percent = None
+
+    def message(self):
+        return "reference mismatch"
+
+
+class FailureReftestMismatchDidNotOccur(TestFailure):
+    def __init__(self, reference_filename=None):
+        super(FailureReftestMismatchDidNotOccur, self).__init__()
+        self.reference_filename = reference_filename
+
+    def message(self):
+        return "reference mismatch didn't happen"
+
+
+class FailureReftestNoImagesGenerated(TestFailure):
+    def __init__(self, reference_filename=None):
+        super(FailureReftestNoImagesGenerated, self).__init__()
+        self.reference_filename = reference_filename
+
+    def message(self):
+        return "reference didn't generate pixel results."
+
+
+class FailureMissingAudio(TestFailure):
+    def message(self):
+        return "expected audio result was missing"
+
+
+class FailureAudioMismatch(TestFailure):
+    def message(self):
+        return "audio mismatch"
+
+
+class FailureEarlyExit(TestFailure):
+    def message(self):
+        return "skipped due to early exit"
+
+
+# Convenient collection of all failure classes for anything that might
+# need to enumerate over them all.
+ALL_FAILURE_CLASSES = (FailureTimeout, FailureCrash, FailureMissingResult,
+                       FailureTextMismatch, FailureMissingImageHash,
+                       FailureMissingImage, FailureImageHashMismatch,
+                       FailureImageHashIncorrect, FailureReftestMismatch,
+                       FailureReftestMismatchDidNotOccur, FailureReftestNoImagesGenerated,
+                       FailureMissingAudio, FailureAudioMismatch,
+                       FailureEarlyExit)
diff --git a/Tools/Scripts/webkitpy/layout_tests/models/test_failures_unittest.py b/Tools/Scripts/webkitpy/layout_tests/models/test_failures_unittest.py
new file mode 100644
index 0000000..1c8f029
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/models/test_failures_unittest.py
@@ -0,0 +1,73 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.layout_tests.models.test_failures import *
+
+
+class TestFailuresTest(unittest.TestCase):
+    def assert_loads(self, cls):
+        failure_obj = cls()
+        s = failure_obj.dumps()
+        new_failure_obj = TestFailure.loads(s)
+        self.assertTrue(isinstance(new_failure_obj, cls))
+
+        self.assertEqual(failure_obj, new_failure_obj)
+
+        # Also test that != is implemented.
+        self.assertFalse(failure_obj != new_failure_obj)
+
+    def test_unknown_failure_type(self):
+        class UnknownFailure(TestFailure):
+            def message(self):
+                return ''
+
+        failure_obj = UnknownFailure()
+        self.assertRaises(ValueError, determine_result_type, [failure_obj])
+
+    def test_message_is_virtual(self):
+        failure_obj = TestFailure()
+        self.assertRaises(NotImplementedError, failure_obj.message)
+
+    def test_loads(self):
+        for c in ALL_FAILURE_CLASSES:
+            self.assert_loads(c)
+
+    def test_equals(self):
+        self.assertEqual(FailureCrash(), FailureCrash())
+        self.assertNotEqual(FailureCrash(), FailureTimeout())
+        crash_set = set([FailureCrash(), FailureCrash()])
+        self.assertEqual(len(crash_set), 1)
+        # The hash happens to be the name of the class, but sets still work:
+        crash_set = set([FailureCrash(), "FailureCrash"])
+        self.assertEqual(len(crash_set), 2)
+
+    def test_crashes(self):
+        self.assertEquals(FailureCrash().message(), 'DumpRenderTree crashed')
+        self.assertEquals(FailureCrash(process_name='foo', pid=1234).message(), 'foo crashed [pid=1234]')
diff --git a/Tools/Scripts/webkitpy/layout_tests/models/test_input.py b/Tools/Scripts/webkitpy/layout_tests/models/test_input.py
new file mode 100644
index 0000000..56f2d52
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/models/test_input.py
@@ -0,0 +1,46 @@
+#!/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
+# 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 TestInput(object):
+    """Groups information about a test for easy passing of data."""
+
+    def __init__(self, test_name, timeout=None, requires_lock=None, reference_files=None, should_run_pixel_tests=None):
+        # TestInput objects are normally constructed by the manager and passed
+        # to the workers, but these some fields are set lazily in the workers where possible
+        # because they require us to look at the filesystem and we want to be able to do that in parallel.
+        self.test_name = test_name
+        self.timeout = timeout  # in msecs; should rename this for consistency
+        self.requires_lock = requires_lock
+        self.reference_files = reference_files
+        self.should_run_pixel_tests = should_run_pixel_tests
+
+    def __repr__(self):
+        return "TestInput('%s', timeout=%s, requires_lock=%s, reference_files=%s, should_run_pixel_tests=%s)" % (self.test_name, self.timeout, self.requires_lock, self.reference_files, self.should_run_pixel_tests)
diff --git a/Tools/Scripts/webkitpy/layout_tests/models/test_results.py b/Tools/Scripts/webkitpy/layout_tests/models/test_results.py
new file mode 100644
index 0000000..6b9db55
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/models/test_results.py
@@ -0,0 +1,65 @@
+# 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 cPickle
+
+from webkitpy.layout_tests.models import test_failures
+
+
+class TestResult(object):
+    """Data object containing the results of a single test."""
+
+    @staticmethod
+    def loads(string):
+        return cPickle.loads(string)
+
+    def __init__(self, test_name, failures=None, test_run_time=None, has_stderr=False, reftest_type=[]):
+        self.test_name = test_name
+        self.failures = failures or []
+        self.test_run_time = test_run_time or 0
+        self.has_stderr = has_stderr
+        self.reftest_type = reftest_type
+        # FIXME: Setting this in the constructor makes this class hard to mutate.
+        self.type = test_failures.determine_result_type(failures)
+
+    def __eq__(self, other):
+        return (self.test_name == other.test_name and
+                self.failures == other.failures and
+                self.test_run_time == other.test_run_time)
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def has_failure_matching_types(self, *failure_classes):
+        for failure in self.failures:
+            if type(failure) in failure_classes:
+                return True
+        return False
+
+    def dumps(self):
+        return cPickle.dumps(self)
diff --git a/Tools/Scripts/webkitpy/layout_tests/models/test_results_unittest.py b/Tools/Scripts/webkitpy/layout_tests/models/test_results_unittest.py
new file mode 100644
index 0000000..80d8a47
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/models/test_results_unittest.py
@@ -0,0 +1,52 @@
+# 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.layout_tests.models.test_results import TestResult
+
+
+class TestResultsTest(unittest.TestCase):
+    def test_defaults(self):
+        result = TestResult("foo")
+        self.assertEqual(result.test_name, 'foo')
+        self.assertEqual(result.failures, [])
+        self.assertEqual(result.test_run_time, 0)
+
+    def test_loads(self):
+        result = TestResult(test_name='foo',
+                            failures=[],
+                            test_run_time=1.1)
+        s = result.dumps()
+        new_result = TestResult.loads(s)
+        self.assertTrue(isinstance(new_result, TestResult))
+
+        self.assertEqual(new_result, result)
+
+        # Also check that != is implemented.
+        self.assertFalse(new_result != result)
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/__init__.py b/Tools/Scripts/webkitpy/layout_tests/port/__init__.py
new file mode 100644
index 0000000..6365b4c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/__init__.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Port-specific entrypoints for the layout tests test infrastructure."""
+
+import builders  # Why is this in port?
+
+from base import Port  # It's possible we don't need to export this virtual baseclass outside the module.
+from driver import Driver, DriverInput, DriverOutput
+from factory import platform_options, configuration_options
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/apple.py b/Tools/Scripts/webkitpy/layout_tests/port/apple.py
new file mode 100644
index 0000000..4b97f41
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/apple.py
@@ -0,0 +1,100 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import logging
+
+from webkitpy.layout_tests.port.base import Port
+from webkitpy.layout_tests.models.test_configuration import TestConfiguration
+
+
+_log = logging.getLogger(__name__)
+
+
+class ApplePort(Port):
+    """Shared logic between all of Apple's ports."""
+
+    # This is used to represent the version of an operating system
+    # corresponding to the "mac" or "win" base LayoutTests/platform
+    # directory.  I'm not sure this concept is very useful,
+    # but it gives us a way to refer to fallback paths *only* including
+    # the base directory.
+    # This is mostly done because TestConfiguration assumes that self.version()
+    # will never return None. (None would be another way to represent this concept.)
+    # Apple supposedly has explicit "future" results which are kept in an internal repository.
+    # It's possible that Apple would want to fix this code to work better with those results.
+    FUTURE_VERSION = 'future'  # FIXME: This whole 'future' thing feels like a hack.
+
+    @classmethod
+    def determine_full_port_name(cls, host, options, port_name):
+        # If the port_name matches the (badly named) cls.port_name, that
+        # means that they passed 'mac' or 'win' and didn't specify a version.
+        # That convention means that we're supposed to use the version currently
+        # being run, so this won't work if you're not on mac or win (respectively).
+        # If you're not on the o/s in question, you must specify a full version or -future (cf. above).
+        if port_name == cls.port_name:
+            assert port_name == host.platform.os_name
+            return cls.port_name + '-' + host.platform.os_version
+        if port_name == cls.port_name + '-wk2':
+            assert port_name == host.platform.os_name + '-wk2'
+            return cls.port_name + '-' + host.platform.os_version + '-wk2'
+        return port_name
+
+    def _strip_port_name_prefix(self, port_name):
+        # Callers treat this return value as the "version", which only works
+        # because Apple ports use a simple name-version port_name scheme.
+        # FIXME: This parsing wouldn't be needed if port_name handling was moved to factory.py
+        # instead of the individual port constructors.
+        return port_name[len(self.port_name + '-'):]
+
+    def __init__(self, host, port_name, **kwargs):
+        super(ApplePort, self).__init__(host, port_name, **kwargs)
+
+        allowed_port_names = self.VERSION_FALLBACK_ORDER + [self.operating_system() + "-future"]
+        port_name = port_name.replace('-wk2', '')
+        self._version = self._strip_port_name_prefix(port_name)
+        assert port_name in allowed_port_names, "%s is not in %s" % (port_name, allowed_port_names)
+
+    def _skipped_file_search_paths(self):
+        # We don't have a dedicated Skipped file for the most recent version of the port;
+        # we just use the one in platform/{mac,win}
+        most_recent_name = self.VERSION_FALLBACK_ORDER[-1]
+        return set(filter(lambda name: name != most_recent_name, super(ApplePort, self)._skipped_file_search_paths()))
+
+    # FIXME: A more sophisticated version of this function should move to WebKitPort and replace all calls to name().
+    # This is also a misleading name, since 'mac-future' gets remapped to 'mac'.
+    def _port_name_with_version(self):
+        return self.name().replace('-future', '').replace('-wk2', '')
+
+    def _generate_all_test_configurations(self):
+        configurations = []
+        allowed_port_names = self.VERSION_FALLBACK_ORDER + [self.operating_system() + "-future"]
+        for port_name in allowed_port_names:
+            for build_type in self.ALL_BUILD_TYPES:
+                for architecture in self.ARCHITECTURES:
+                    configurations.append(TestConfiguration(version=self._strip_port_name_prefix(port_name), architecture=architecture, build_type=build_type))
+        return configurations
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/base.py b/Tools/Scripts/webkitpy/layout_tests/port/base.py
new file mode 100755
index 0000000..ea1e9d0
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/base.py
@@ -0,0 +1,1520 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Abstract base class of Port-specific entry points for the layout tests
+test infrastructure (the Port and Driver classes)."""
+
+import cgi
+import difflib
+import errno
+import itertools
+import logging
+import os
+import operator
+import optparse
+import re
+import sys
+
+try:
+    from collections import OrderedDict
+except ImportError:
+    # Needed for Python < 2.7
+    from webkitpy.thirdparty.ordered_dict import OrderedDict
+
+
+from webkitpy.common import find_files
+from webkitpy.common import read_checksum_from_png
+from webkitpy.common.memoized import memoized
+from webkitpy.common.system import path
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.system.systemhost import SystemHost
+from webkitpy.layout_tests.models.test_configuration import TestConfiguration
+from webkitpy.layout_tests.port import config as port_config
+from webkitpy.layout_tests.port import driver
+from webkitpy.layout_tests.port import http_lock
+from webkitpy.layout_tests.port import image_diff
+from webkitpy.layout_tests.port import server_process
+from webkitpy.layout_tests.port.factory import PortFactory
+from webkitpy.layout_tests.servers import apache_http_server
+from webkitpy.layout_tests.servers import http_server
+from webkitpy.layout_tests.servers import websocket_server
+
+_log = logging.getLogger(__name__)
+
+
+# FIXME: This class should merge with WebKitPort now that Chromium behaves mostly like other webkit ports.
+class Port(object):
+    """Abstract class for Port-specific hooks for the layout_test package."""
+
+    # Subclasses override this. This should indicate the basic implementation
+    # part of the port name, e.g., 'chromium-mac', 'win', 'gtk'; there is probably (?)
+    # one unique value per class.
+
+    # FIXME: We should probably rename this to something like 'implementation_name'.
+    port_name = None
+
+    # Test names resemble unix relative paths, and use '/' as a directory separator.
+    TEST_PATH_SEPARATOR = '/'
+
+    ALL_BUILD_TYPES = ('debug', 'release')
+
+    @classmethod
+    def determine_full_port_name(cls, host, options, port_name):
+        """Return a fully-specified port name that can be used to construct objects."""
+        # Subclasses will usually override this.
+        return cls.port_name
+
+    def __init__(self, host, port_name=None, options=None, config=None, **kwargs):
+
+        # This value may be different from cls.port_name by having version modifiers
+        # and other fields appended to it (for example, 'qt-arm' or 'mac-wk2').
+
+        # FIXME: port_name should be a required parameter. It isn't yet because lots of tests need to be updatd.
+        self._name = port_name or self.port_name
+
+        # These are default values that should be overridden in a subclasses.
+        self._version = ''
+        self._architecture = 'x86'
+
+        # 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 = options or optparse.Values()
+
+        self.host = host
+        self._executive = host.executive
+        self._filesystem = host.filesystem
+        self._config = config or port_config.Config(self._executive, self._filesystem, self.port_name)
+
+        self._helper = None
+        self._http_server = None
+        self._websocket_server = None
+        self._image_differ = None
+        self._server_process_constructor = server_process.ServerProcess  # overridable for testing
+        self._http_lock = None  # FIXME: Why does this live on the port object?
+
+        # 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 = None
+
+        # FIXME: prettypatch.py knows this path, why is it copied here?
+        self._pretty_patch_path = self.path_from_webkit_base("Websites", "bugs.webkit.org", "PrettyPatch", "prettify.rb")
+        self._pretty_patch_available = None
+
+        if not hasattr(options, 'configuration') or not options.configuration:
+            self.set_option_default('configuration', self.default_configuration())
+        self._test_configuration = None
+        self._reftest_list = {}
+        self._results_directory = None
+        self._root_was_set = hasattr(options, 'root') and options.root
+
+    def additional_drt_flag(self):
+        return []
+
+    def default_pixel_tests(self):
+        # FIXME: Disable until they are run by default on build.webkit.org.
+        return False
+
+    def default_timeout_ms(self):
+        if self.get_option('webkit_test_runner'):
+            # Add some more time to WebKitTestRunner because it needs to syncronise the state
+            # with the web process and we want to detect if there is a problem with that in the driver.
+            return 80 * 1000
+        return 35 * 1000
+
+    def driver_stop_timeout(self):
+        """ Returns the amount of time in seconds to wait before killing the process in driver.stop()."""
+        # We want to wait for at least 3 seconds, but if we are really slow, we want to be slow on cleanup as
+        # well (for things like ASAN, Valgrind, etc.)
+        return 3.0 * float(self.get_option('time_out_ms', '0')) / self.default_timeout_ms()
+
+    def wdiff_available(self):
+        if self._wdiff_available is None:
+            self._wdiff_available = self.check_wdiff(logging=False)
+        return self._wdiff_available
+
+    def pretty_patch_available(self):
+        if self._pretty_patch_available is None:
+            self._pretty_patch_available = self.check_pretty_patch(logging=False)
+        return self._pretty_patch_available
+
+    def should_retry_crashes(self):
+        return False
+
+    def default_child_processes(self):
+        """Return the number of DumpRenderTree instances to use for this port."""
+        return self._executive.cpu_count()
+
+    def default_max_locked_shards(self):
+        """Return the number of "locked" shards to run in parallel (like the http tests)."""
+        return 1
+
+    def worker_startup_delay_secs(self):
+        # FIXME: If we start workers up too quickly, DumpRenderTree appears
+        # to thrash on something and time out its first few tests. Until
+        # we can figure out what's going on, sleep a bit in between
+        # workers. See https://bugs.webkit.org/show_bug.cgi?id=79147 .
+        return 0.1
+
+    def baseline_path(self):
+        """Return the absolute path to the directory to store new baselines in for this port."""
+        # FIXME: remove once all callers are calling either baseline_version_dir() or baseline_platform_dir()
+        return self.baseline_version_dir()
+
+    def baseline_platform_dir(self):
+        """Return the absolute path to the default (version-independent) platform-specific results."""
+        return self._filesystem.join(self.layout_tests_dir(), 'platform', self.port_name)
+
+    def baseline_version_dir(self):
+        """Return the absolute path to the platform-and-version-specific results."""
+        baseline_search_paths = self.baseline_search_path()
+        return baseline_search_paths[0]
+
+    def baseline_search_path(self):
+        return self.get_option('additional_platform_directory', []) + self._compare_baseline() + self.default_baseline_search_path()
+
+    def default_baseline_search_path(self):
+        """Return a list of absolute paths to directories to search under for
+        baselines. The directories are searched in order."""
+        search_paths = []
+        if self.get_option('webkit_test_runner'):
+            search_paths.append(self._wk2_port_name())
+        search_paths.append(self.name())
+        if self.name() != self.port_name:
+            search_paths.append(self.port_name)
+        return map(self._webkit_baseline_path, search_paths)
+
+    @memoized
+    def _compare_baseline(self):
+        factory = PortFactory(self.host)
+        target_port = self.get_option('compare_port')
+        if target_port:
+            return factory.get(target_port).default_baseline_search_path()
+        return []
+
+    def check_build(self, needs_http):
+        """This routine is used to ensure that the build is up to date
+        and all the needed binaries are present."""
+        # If we're using a pre-built copy of WebKit (--root), we assume it also includes a build of DRT.
+        if not self._root_was_set and self.get_option('build') and not self._build_driver():
+            return False
+        if not self._check_driver():
+            return False
+        if self.get_option('pixel_tests'):
+            if not self.check_image_diff():
+                return False
+        if not self._check_port_build():
+            return False
+        return True
+
+    def _check_driver(self):
+        driver_path = self._path_to_driver()
+        if not self._filesystem.exists(driver_path):
+            _log.error("%s was not found at %s" % (self.driver_name(), driver_path))
+            return False
+        return True
+
+    def _check_port_build(self):
+        # Ports can override this method to do additional checks.
+        return True
+
+    def check_sys_deps(self, needs_http):
+        """If the port needs to do some runtime checks to ensure that the
+        tests can be run successfully, it should override this routine.
+        This step can be skipped with --nocheck-sys-deps.
+
+        Returns whether the system is properly configured."""
+        if needs_http:
+            return self.check_httpd()
+        return True
+
+    def check_image_diff(self, override_step=None, logging=True):
+        """This routine is used to check whether image_diff binary exists."""
+        image_diff_path = self._path_to_image_diff()
+        if not self._filesystem.exists(image_diff_path):
+            _log.error("ImageDiff was not found at %s" % image_diff_path)
+            return False
+        return True
+
+    def check_pretty_patch(self, logging=True):
+        """Checks whether we can use the PrettyPatch ruby script."""
+        try:
+            _ = self._executive.run_command(['ruby', '--version'])
+        except OSError, e:
+            if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]:
+                if logging:
+                    _log.warning("Ruby is not installed; can't generate pretty patches.")
+                    _log.warning('')
+                return False
+
+        if not self._filesystem.exists(self._pretty_patch_path):
+            if logging:
+                _log.warning("Unable to find %s; can't generate pretty patches." % self._pretty_patch_path)
+                _log.warning('')
+            return False
+
+        return True
+
+    def check_wdiff(self, logging=True):
+        if not self._path_to_wdiff():
+            # Don't need to log here since this is the port choosing not to use wdiff.
+            return False
+
+        try:
+            _ = self._executive.run_command([self._path_to_wdiff(), '--help'])
+        except OSError:
+            if logging:
+                message = self._wdiff_missing_message()
+                if message:
+                    for line in message.splitlines():
+                        _log.warning('    ' + line)
+                        _log.warning('')
+            return False
+
+        return True
+
+    def _wdiff_missing_message(self):
+        return 'wdiff is not installed; please install it to generate word-by-word diffs.'
+
+    def check_httpd(self):
+        if self._uses_apache():
+            httpd_path = self._path_to_apache()
+        else:
+            httpd_path = self._path_to_lighttpd()
+
+        try:
+            server_name = self._filesystem.basename(httpd_path)
+            env = self.setup_environ_for_server(server_name)
+            if self._executive.run_command([httpd_path, "-v"], env=env, return_exit_code=True) != 0:
+                _log.error("httpd seems broken. Cannot run http tests.")
+                return False
+            return True
+        except OSError:
+            _log.error("No httpd found. Cannot run http tests.")
+            return False
+
+    def do_text_results_differ(self, expected_text, actual_text):
+        return expected_text != actual_text
+
+    def do_audio_results_differ(self, expected_audio, actual_audio):
+        return expected_audio != actual_audio
+
+    def diff_image(self, expected_contents, actual_contents, tolerance=None):
+        """Compare two images and return a tuple of an image diff, a percentage difference (0-100), and an error string.
+
+        |tolerance| should be a percentage value (0.0 - 100.0).
+        If it is omitted, the port default tolerance value is used.
+
+        If an error occurs (like ImageDiff isn't found, or crashes, we log an error and return True (for a diff).
+        """
+        if not actual_contents and not expected_contents:
+            return (None, 0, None)
+        if not actual_contents or not expected_contents:
+            return (True, 0, None)
+        if not self._image_differ:
+            self._image_differ = image_diff.ImageDiffer(self)
+        self.set_option_default('tolerance', 0.1)
+        if tolerance is None:
+            tolerance = self.get_option('tolerance')
+        return self._image_differ.diff_image(expected_contents, actual_contents, tolerance)
+
+    def diff_text(self, expected_text, actual_text, expected_filename, actual_filename):
+        """Returns a string containing the diff of the two text strings
+        in 'unified diff' format."""
+
+        # The filenames show up in the diff output, make sure they're
+        # raw bytes and not unicode, so that they don't trigger join()
+        # trying to decode the input.
+        def to_raw_bytes(string_value):
+            if isinstance(string_value, unicode):
+                return string_value.encode('utf-8')
+            return string_value
+        expected_filename = to_raw_bytes(expected_filename)
+        actual_filename = to_raw_bytes(actual_filename)
+        diff = difflib.unified_diff(expected_text.splitlines(True),
+                                    actual_text.splitlines(True),
+                                    expected_filename,
+                                    actual_filename)
+        return ''.join(diff)
+
+    def check_for_leaks(self, process_name, process_pid):
+        # Subclasses should check for leaks in the running process
+        # and print any necessary warnings if leaks are found.
+        # FIXME: We should consider moving much of this logic into
+        # Executive and make it platform-specific instead of port-specific.
+        pass
+
+    def print_leaks_summary(self):
+        # Subclasses can override this to print a summary of leaks found
+        # while running the layout tests.
+        pass
+
+    def driver_name(self):
+        if self.get_option('driver_name'):
+            return self.get_option('driver_name')
+        if self.get_option('webkit_test_runner'):
+            return 'WebKitTestRunner'
+        return 'DumpRenderTree'
+
+    def expected_baselines_by_extension(self, test_name):
+        """Returns a dict mapping baseline suffix to relative path for each baseline in
+        a test. For reftests, it returns ".==" or ".!=" instead of the suffix."""
+        # FIXME: The name similarity between this and expected_baselines() below, is unfortunate.
+        # We should probably rename them both.
+        baseline_dict = {}
+        reference_files = self.reference_files(test_name)
+        if reference_files:
+            # FIXME: How should this handle more than one type of reftest?
+            baseline_dict['.' + reference_files[0][0]] = self.relative_test_filename(reference_files[0][1])
+
+        for extension in self.baseline_extensions():
+            path = self.expected_filename(test_name, extension, return_default=False)
+            baseline_dict[extension] = self.relative_test_filename(path) if path else path
+
+        return baseline_dict
+
+    def baseline_extensions(self):
+        """Returns a tuple of all of the non-reftest baseline extensions we use. The extensions include the leading '.'."""
+        return ('.wav', '.webarchive', '.txt', '.png')
+
+    def expected_baselines(self, test_name, suffix, all_baselines=False):
+        """Given a test name, finds where the baseline results are located.
+
+        Args:
+        test_name: name of test file (usually a relative path under LayoutTests/)
+        suffix: file suffix of the expected results, including dot; e.g.
+            '.txt' or '.png'.  This should not be None, but may be an empty
+            string.
+        all_baselines: If True, return an ordered list of all baseline paths
+            for the given platform. If False, return only the first one.
+        Returns
+        a list of ( platform_dir, results_filename ), where
+            platform_dir - abs path to the top of the results tree (or test
+                tree)
+            results_filename - relative path from top of tree to the results
+                file
+            (port.join() of the two gives you the full path to the file,
+                unless None was returned.)
+        Return values will be in the format appropriate for the current
+        platform (e.g., "\\" for path separators on Windows). If the results
+        file is not found, then None will be returned for the directory,
+        but the expected relative pathname will still be returned.
+
+        This routine is generic but lives here since it is used in
+        conjunction with the other baseline and filename routines that are
+        platform specific.
+        """
+        baseline_filename = self._filesystem.splitext(test_name)[0] + '-expected' + suffix
+        baseline_search_path = self.baseline_search_path()
+
+        baselines = []
+        for platform_dir in baseline_search_path:
+            if self._filesystem.exists(self._filesystem.join(platform_dir, baseline_filename)):
+                baselines.append((platform_dir, baseline_filename))
+
+            if not all_baselines and baselines:
+                return baselines
+
+        # If it wasn't found in a platform directory, return the expected
+        # result in the test directory, even if no such file actually exists.
+        platform_dir = self.layout_tests_dir()
+        if self._filesystem.exists(self._filesystem.join(platform_dir, baseline_filename)):
+            baselines.append((platform_dir, baseline_filename))
+
+        if baselines:
+            return baselines
+
+        return [(None, baseline_filename)]
+
+    def expected_filename(self, test_name, suffix, return_default=True):
+        """Given a test name, returns an absolute path to its expected results.
+
+        If no expected results are found in any of the searched directories,
+        the directory in which the test itself is located will be returned.
+        The return value is in the format appropriate for the platform
+        (e.g., "\\" for path separators on windows).
+
+        Args:
+        test_name: name of test file (usually a relative path under LayoutTests/)
+        suffix: file suffix of the expected results, including dot; e.g. '.txt'
+            or '.png'.  This should not be None, but may be an empty string.
+        platform: the most-specific directory name to use to build the
+            search list of directories, e.g., 'chromium-win', or
+            'chromium-cg-mac-leopard' (we follow the WebKit format)
+        return_default: if True, returns the path to the generic expectation if nothing
+            else is found; if False, returns None.
+
+        This routine is generic but is implemented here to live alongside
+        the other baseline and filename manipulation routines.
+        """
+        # FIXME: The [0] here is very mysterious, as is the destructured return.
+        platform_dir, baseline_filename = self.expected_baselines(test_name, suffix)[0]
+        if platform_dir:
+            return self._filesystem.join(platform_dir, baseline_filename)
+
+        actual_test_name = self.lookup_virtual_test_base(test_name)
+        if actual_test_name:
+            return self.expected_filename(actual_test_name, suffix)
+
+        if return_default:
+            return self._filesystem.join(self.layout_tests_dir(), baseline_filename)
+        return None
+
+    def expected_checksum(self, test_name):
+        """Returns the checksum of the image we expect the test to produce, or None if it is a text-only test."""
+        png_path = self.expected_filename(test_name, '.png')
+
+        if self._filesystem.exists(png_path):
+            with self._filesystem.open_binary_file_for_reading(png_path) as filehandle:
+                return read_checksum_from_png.read_checksum(filehandle)
+
+        return None
+
+    def expected_image(self, test_name):
+        """Returns the image we expect the test to produce."""
+        baseline_path = self.expected_filename(test_name, '.png')
+        if not self._filesystem.exists(baseline_path):
+            return None
+        return self._filesystem.read_binary_file(baseline_path)
+
+    def expected_audio(self, test_name):
+        baseline_path = self.expected_filename(test_name, '.wav')
+        if not self._filesystem.exists(baseline_path):
+            return None
+        return self._filesystem.read_binary_file(baseline_path)
+
+    def expected_text(self, test_name):
+        """Returns the text output we expect the test to produce, or None
+        if we don't expect there to be any text output.
+        End-of-line characters are normalized to '\n'."""
+        # FIXME: DRT output is actually utf-8, but since we don't decode the
+        # output from DRT (instead treating it as a binary string), we read the
+        # baselines as a binary string, too.
+        baseline_path = self.expected_filename(test_name, '.txt')
+        if not self._filesystem.exists(baseline_path):
+            baseline_path = self.expected_filename(test_name, '.webarchive')
+            if not self._filesystem.exists(baseline_path):
+                return None
+        text = self._filesystem.read_binary_file(baseline_path)
+        return text.replace("\r\n", "\n")
+
+    def _get_reftest_list(self, test_name):
+        dirname = self._filesystem.join(self.layout_tests_dir(), self._filesystem.dirname(test_name))
+        if dirname not in self._reftest_list:
+            self._reftest_list[dirname] = Port._parse_reftest_list(self._filesystem, dirname)
+        return self._reftest_list[dirname]
+
+    @staticmethod
+    def _parse_reftest_list(filesystem, test_dirpath):
+        reftest_list_path = filesystem.join(test_dirpath, 'reftest.list')
+        if not filesystem.isfile(reftest_list_path):
+            return None
+        reftest_list_file = filesystem.read_text_file(reftest_list_path)
+
+        parsed_list = {}
+        for line in reftest_list_file.split('\n'):
+            line = re.sub('#.+$', '', line)
+            split_line = line.split()
+            if len(split_line) < 3:
+                continue
+            expectation_type, test_file, ref_file = split_line
+            parsed_list.setdefault(filesystem.join(test_dirpath, test_file), []).append((expectation_type, filesystem.join(test_dirpath, ref_file)))
+        return parsed_list
+
+    def reference_files(self, test_name):
+        """Return a list of expectation (== or !=) and filename pairs"""
+
+        reftest_list = self._get_reftest_list(test_name)
+        if not reftest_list:
+            reftest_list = []
+            for expectation, prefix in (('==', ''), ('!=', '-mismatch')):
+                for extention in Port._supported_file_extensions:
+                    path = self.expected_filename(test_name, prefix + extention)
+                    if self._filesystem.exists(path):
+                        reftest_list.append((expectation, path))
+            return reftest_list
+
+        return reftest_list.get(self._filesystem.join(self.layout_tests_dir(), test_name), [])
+
+    def tests(self, paths):
+        """Return the list of tests found. Both generic and platform-specific tests matching paths should be returned."""
+        expanded_paths = self._expanded_paths(paths)
+        return self._real_tests(expanded_paths).union(self._virtual_tests(expanded_paths, self.populated_virtual_test_suites()))
+
+    def _expanded_paths(self, paths):
+        expanded_paths = []
+        fs = self._filesystem
+        all_platform_dirs = [path for path in fs.glob(fs.join(self.layout_tests_dir(), 'platform', '*')) if fs.isdir(path)]
+        for path in paths:
+            expanded_paths.append(path)
+            if self.test_isdir(path) and not path.startswith('platform'):
+                for platform_dir in all_platform_dirs:
+                    if fs.isdir(fs.join(platform_dir, path)) and platform_dir in self.baseline_search_path():
+                        expanded_paths.append(self.relative_test_filename(fs.join(platform_dir, path)))
+
+        return expanded_paths
+
+    def _real_tests(self, paths):
+        # When collecting test cases, skip these directories
+        skipped_directories = set(['.svn', '_svn', 'resources', 'script-tests', 'reference', 'reftest'])
+        files = find_files.find(self._filesystem, self.layout_tests_dir(), paths, skipped_directories, Port._is_test_file)
+        return set([self.relative_test_filename(f) for f in files])
+
+    # When collecting test cases, we include any file with these extensions.
+    _supported_file_extensions = set(['.html', '.shtml', '.xml', '.xhtml', '.pl',
+                                      '.htm', '.php', '.svg', '.mht'])
+
+    @staticmethod
+    def is_reference_html_file(filesystem, dirname, filename):
+        if filename.startswith('ref-') or filename.endswith('notref-'):
+            return True
+        filename_wihout_ext, unused = filesystem.splitext(filename)
+        for suffix in ['-expected', '-expected-mismatch', '-ref', '-notref']:
+            if filename_wihout_ext.endswith(suffix):
+                return True
+        return False
+
+    @staticmethod
+    def _has_supported_extension(filesystem, filename):
+        """Return true if filename is one of the file extensions we want to run a test on."""
+        extension = filesystem.splitext(filename)[1]
+        return extension in Port._supported_file_extensions
+
+    @staticmethod
+    def _is_test_file(filesystem, dirname, filename):
+        return Port._has_supported_extension(filesystem, filename) and not Port.is_reference_html_file(filesystem, dirname, filename)
+
+    def test_dirs(self):
+        """Returns the list of top-level test directories."""
+        layout_tests_dir = self.layout_tests_dir()
+        return filter(lambda x: self._filesystem.isdir(self._filesystem.join(layout_tests_dir, x)),
+                      self._filesystem.listdir(layout_tests_dir))
+
+    @memoized
+    def test_isfile(self, test_name):
+        """Return True if the test name refers to a directory of tests."""
+        # Used by test_expectations.py to apply rules to whole directories.
+        if self._filesystem.isfile(self.abspath_for_test(test_name)):
+            return True
+        base = self.lookup_virtual_test_base(test_name)
+        return base and self._filesystem.isfile(self.abspath_for_test(base))
+
+    @memoized
+    def test_isdir(self, test_name):
+        """Return True if the test name refers to a directory of tests."""
+        # Used by test_expectations.py to apply rules to whole directories.
+        if self._filesystem.isdir(self.abspath_for_test(test_name)):
+            return True
+        base = self.lookup_virtual_test_base(test_name)
+        return base and self._filesystem.isdir(self.abspath_for_test(base))
+
+    @memoized
+    def test_exists(self, test_name):
+        """Return True if the test name refers to an existing test or baseline."""
+        # Used by test_expectations.py to determine if an entry refers to a
+        # valid test and by printing.py to determine if baselines exist.
+        return self.test_isfile(test_name) or self.test_isdir(test_name)
+
+    def split_test(self, test_name):
+        """Splits a test name into the 'directory' part and the 'basename' part."""
+        index = test_name.rfind(self.TEST_PATH_SEPARATOR)
+        if index < 1:
+            return ('', test_name)
+        return (test_name[0:index], test_name[index:])
+
+    def normalize_test_name(self, test_name):
+        """Returns a normalized version of the test name or test directory."""
+        if test_name.endswith('/'):
+            return test_name
+        if self.test_isdir(test_name):
+            return test_name + '/'
+        return test_name
+
+    def driver_cmd_line(self):
+        """Prints the DRT command line that will be used."""
+        driver = self.create_driver(0)
+        return driver.cmd_line(self.get_option('pixel_tests'), [])
+
+    def update_baseline(self, baseline_path, data):
+        """Updates the baseline for a test.
+
+        Args:
+            baseline_path: the actual path to use for baseline, not the path to
+              the test. This function is used to update either generic or
+              platform-specific baselines, but we can't infer which here.
+            data: contents of the baseline.
+        """
+        self._filesystem.write_binary_file(baseline_path, data)
+
+    @memoized
+    def layout_tests_dir(self):
+        """Return the absolute path to the top of the LayoutTests directory."""
+        return self._filesystem.normpath(self.path_from_webkit_base('LayoutTests'))
+
+    def perf_tests_dir(self):
+        """Return the absolute path to the top of the PerformanceTests directory."""
+        return self.path_from_webkit_base('PerformanceTests')
+
+    def webkit_base(self):
+        return self._filesystem.abspath(self.path_from_webkit_base('.'))
+
+    def skipped_layout_tests(self, test_list):
+        """Returns tests skipped outside of the TestExpectations files."""
+        return set(self._tests_for_other_platforms()).union(self._skipped_tests_for_unsupported_features(test_list))
+
+    def _tests_from_skipped_file_contents(self, skipped_file_contents):
+        tests_to_skip = []
+        for line in skipped_file_contents.split('\n'):
+            line = line.strip()
+            line = line.rstrip('/')  # Best to normalize directory names to not include the trailing slash.
+            if line.startswith('#') or not len(line):
+                continue
+            tests_to_skip.append(line)
+        return tests_to_skip
+
+    def _expectations_from_skipped_files(self, skipped_file_paths):
+        tests_to_skip = []
+        for search_path in skipped_file_paths:
+            filename = self._filesystem.join(self._webkit_baseline_path(search_path), "Skipped")
+            if not self._filesystem.exists(filename):
+                _log.debug("Skipped does not exist: %s" % filename)
+                continue
+            _log.debug("Using Skipped file: %s" % filename)
+            skipped_file_contents = self._filesystem.read_text_file(filename)
+            tests_to_skip.extend(self._tests_from_skipped_file_contents(skipped_file_contents))
+        return tests_to_skip
+
+    @memoized
+    def skipped_perf_tests(self):
+        return self._expectations_from_skipped_files([self.perf_tests_dir()])
+
+    def skips_perf_test(self, test_name):
+        for test_or_category in self.skipped_perf_tests():
+            if test_or_category == test_name:
+                return True
+            category = self._filesystem.join(self.perf_tests_dir(), test_or_category)
+            if self._filesystem.isdir(category) and test_name.startswith(test_or_category):
+                return True
+        return False
+
+    def is_chromium(self):
+        return False
+
+    def name(self):
+        """Returns a name that uniquely identifies this particular type of port
+        (e.g., "mac-snowleopard" or "chromium-linux-x86_x64" and can be passed
+        to factory.get() to instantiate the port."""
+        return self._name
+
+    def operating_system(self):
+        # Subclasses should override this default implementation.
+        return 'mac'
+
+    def version(self):
+        """Returns a string indicating the version of a given platform, e.g.
+        'leopard' or 'xp'.
+
+        This is used to help identify the exact port when parsing test
+        expectations, determining search paths, and logging information."""
+        return self._version
+
+    def architecture(self):
+        return self._architecture
+
+    def get_option(self, name, default_value=None):
+        return getattr(self._options, name, default_value)
+
+    def set_option_default(self, name, default_value):
+        return self._options.ensure_value(name, default_value)
+
+    def path_from_webkit_base(self, *comps):
+        """Returns the full path to path made by joining the top of the
+        WebKit source tree and the list of path components in |*comps|."""
+        return self._config.path_from_webkit_base(*comps)
+
+    @memoized
+    def path_to_test_expectations_file(self):
+        """Update the test expectations to the passed-in string.
+
+        This is used by the rebaselining tool. Raises NotImplementedError
+        if the port does not use expectations files."""
+
+        # FIXME: We need to remove this when we make rebaselining work with multiple files and just generalize expectations_files().
+
+        # test_expectations are always in mac/ not mac-leopard/ by convention, hence we use port_name instead of name().
+        port_name = self.port_name
+        if port_name.startswith('chromium'):
+            port_name = 'chromium'
+
+        return self._filesystem.join(self._webkit_baseline_path(port_name), 'TestExpectations')
+
+    def relative_test_filename(self, filename):
+        """Returns a test_name a relative unix-style path for a filename under the LayoutTests
+        directory. Ports may legitimately return abspaths here if no relpath makes sense."""
+        # Ports that run on windows need to override this method to deal with
+        # filenames with backslashes in them.
+        if filename.startswith(self.layout_tests_dir()):
+            return self.host.filesystem.relpath(filename, self.layout_tests_dir())
+        else:
+            return self.host.filesystem.abspath(filename)
+
+    def relative_perf_test_filename(self, filename):
+        if filename.startswith(self.perf_tests_dir()):
+            return self.host.filesystem.relpath(filename, self.perf_tests_dir())
+        else:
+            return self.host.filesystem.abspath(filename)
+
+    @memoized
+    def abspath_for_test(self, test_name):
+        """Returns the full path to the file for a given test name. This is the
+        inverse of relative_test_filename()."""
+        return self._filesystem.join(self.layout_tests_dir(), test_name)
+
+    def results_directory(self):
+        """Absolute path to the place to store the test results (uses --results-directory)."""
+        if not self._results_directory:
+            option_val = self.get_option('results_directory') or self.default_results_directory()
+            self._results_directory = self._filesystem.abspath(option_val)
+        return self._results_directory
+
+    def perf_results_directory(self):
+        return self._build_path()
+
+    def default_results_directory(self):
+        """Absolute path to the default place to store the test results."""
+        # 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('layout-test-results')
+
+    def setup_test_run(self):
+        """Perform port-specific work at the beginning of a test run."""
+        pass
+
+    def clean_up_test_run(self):
+        """Perform port-specific work at the end of a test run."""
+        if self._image_differ:
+            self._image_differ.stop()
+            self._image_differ = None
+
+    # FIXME: os.environ access should be moved to onto a common/system class to be more easily mockable.
+    def _value_or_default_from_environ(self, name, default=None):
+        if name in os.environ:
+            return os.environ[name]
+        return default
+
+    def _copy_value_from_environ_if_set(self, clean_env, name):
+        if name in os.environ:
+            clean_env[name] = os.environ[name]
+
+    def setup_environ_for_server(self, server_name=None):
+        # We intentionally copy only a subset of os.environ when
+        # launching subprocesses to ensure consistent test results.
+        clean_env = {}
+        variables_to_copy = [
+            # For Linux:
+            'XAUTHORITY',
+            'HOME',
+            'LANG',
+            'LD_LIBRARY_PATH',
+            'DBUS_SESSION_BUS_ADDRESS',
+            'XDG_DATA_DIRS',
+
+            # Darwin:
+            'DYLD_LIBRARY_PATH',
+            'HOME',
+
+            # CYGWIN:
+            'HOMEDRIVE',
+            'HOMEPATH',
+            '_NT_SYMBOL_PATH',
+
+            # Windows:
+            'PATH',
+
+            # Most ports (?):
+            'WEBKIT_TESTFONTS',
+            'WEBKITOUTPUTDIR',
+        ]
+        for variable in variables_to_copy:
+            self._copy_value_from_environ_if_set(clean_env, variable)
+
+        # For Linux:
+        clean_env['DISPLAY'] = self._value_or_default_from_environ('DISPLAY', ':1')
+
+        for string_variable in self.get_option('additional_env_var', []):
+            [name, value] = string_variable.split('=', 1)
+            clean_env[name] = value
+
+        return clean_env
+
+    def show_results_html_file(self, results_filename):
+        """This routine should display the HTML file pointed at by
+        results_filename in a users' browser."""
+        return self.host.user.open_url(path.abspath_to_uri(self.host.platform, results_filename))
+
+    def create_driver(self, worker_number, no_timeout=False):
+        """Return a newly created Driver subclass for starting/stopping the test driver."""
+        return driver.DriverProxy(self, worker_number, self._driver_class(), pixel_tests=self.get_option('pixel_tests'), no_timeout=no_timeout)
+
+    def start_helper(self):
+        """If a port needs to reconfigure graphics settings or do other
+        things to ensure a known test configuration, it should override this
+        method."""
+        pass
+
+    def requires_http_server(self):
+        """Does the port require an HTTP server for running tests? This could
+        be the case when the tests aren't run on the host platform."""
+        return False
+
+    def start_http_server(self, additional_dirs=None, number_of_servers=None):
+        """Start a web server. Raise an error if it can't start or is already running.
+
+        Ports can stub this out if they don't need a web server to be running."""
+        assert not self._http_server, 'Already running an http server.'
+
+        if self._uses_apache():
+            server = apache_http_server.LayoutTestApacheHttpd(self, self.results_directory(), additional_dirs=additional_dirs, number_of_servers=number_of_servers)
+        else:
+            server = http_server.Lighttpd(self, self.results_directory(), additional_dirs=additional_dirs, number_of_servers=number_of_servers)
+
+        server.start()
+        self._http_server = server
+
+    def start_websocket_server(self):
+        """Start a web server. Raise an error if it can't start or is already running.
+
+        Ports can stub this out if they don't need a websocket server to be running."""
+        assert not self._websocket_server, 'Already running a websocket server.'
+
+        server = websocket_server.PyWebSocket(self, self.results_directory())
+        server.start()
+        self._websocket_server = server
+
+    def http_server_supports_ipv6(self):
+        # Cygwin is the only platform to still use Apache 1.3, which only supports IPV4.
+        # Once it moves to Apache 2, we can drop this method altogether.
+        if self.host.platform.is_cygwin():
+            return False
+        return True
+
+    def acquire_http_lock(self):
+        self._http_lock = http_lock.HttpLock(None, filesystem=self._filesystem, executive=self._executive)
+        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()
+        it must override this routine as well."""
+        pass
+
+    def stop_http_server(self):
+        """Shut down the http server if it is running. Do nothing if it isn't."""
+        if self._http_server:
+            self._http_server.stop()
+            self._http_server = None
+
+    def stop_websocket_server(self):
+        """Shut down the websocket server if it is running. Do nothing if it isn't."""
+        if self._websocket_server:
+            self._websocket_server.stop()
+            self._websocket_server = None
+
+    def release_http_lock(self):
+        if self._http_lock:
+            self._http_lock.cleanup_http_lock()
+
+    def exit_code_from_summarized_results(self, unexpected_results):
+        """Given summarized results, compute the exit code to be returned by new-run-webkit-tests.
+        Bots turn red when this function returns a non-zero value. By default, return the number of regressions
+        to avoid turning bots red by flaky failures, unexpected passes, and missing results"""
+        # Don't turn bots red for flaky failures, unexpected passes, and missing results.
+        return unexpected_results['num_regressions']
+
+    #
+    # TEST EXPECTATION-RELATED METHODS
+    #
+
+    def test_configuration(self):
+        """Returns the current TestConfiguration for the port."""
+        if not self._test_configuration:
+            self._test_configuration = TestConfiguration(self._version, self._architecture, self._options.configuration.lower())
+        return self._test_configuration
+
+    # FIXME: Belongs on a Platform object.
+    @memoized
+    def all_test_configurations(self):
+        """Returns a list of TestConfiguration instances, representing all available
+        test configurations for this port."""
+        return self._generate_all_test_configurations()
+
+    # FIXME: Belongs on a Platform object.
+    def configuration_specifier_macros(self):
+        """Ports may provide a way to abbreviate configuration specifiers to conveniently
+        refer to them as one term or alias specific values to more generic ones. For example:
+
+        (xp, vista, win7) -> win # Abbreviate all Windows versions into one namesake.
+        (lucid) -> linux  # Change specific name of the Linux distro to a more generic term.
+
+        Returns a dictionary, each key representing a macro term ('win', for example),
+        and value being a list of valid configuration specifiers (such as ['xp', 'vista', 'win7'])."""
+        return {}
+
+    def all_baseline_variants(self):
+        """Returns a list of platform names sufficient to cover all the baselines.
+
+        The list should be sorted so that a later platform  will reuse
+        an earlier platform's baselines if they are the same (e.g.,
+        'snowleopard' should precede 'leopard')."""
+        raise NotImplementedError
+
+    def uses_test_expectations_file(self):
+        # This is different from checking test_expectations() is None, because
+        # some ports have Skipped files which are returned as part of test_expectations().
+        return self._filesystem.exists(self.path_to_test_expectations_file())
+
+    def warn_if_bug_missing_in_test_expectations(self):
+        return False
+
+    def expectations_dict(self):
+        """Returns an OrderedDict of name -> expectations strings.
+        The names are expected to be (but not required to be) paths in the filesystem.
+        If the name is a path, the file can be considered updatable for things like rebaselining,
+        so don't use names that are paths if they're not paths.
+        Generally speaking the ordering should be files in the filesystem in cascade order
+        (TestExpectations followed by Skipped, if the port honors both formats),
+        then any built-in expectations (e.g., from compile-time exclusions), then --additional-expectations options."""
+        # FIXME: rename this to test_expectations() once all the callers are updated to know about the ordered dict.
+        expectations = OrderedDict()
+
+        for path in self.expectations_files():
+            if self._filesystem.exists(path):
+                expectations[path] = self._filesystem.read_text_file(path)
+
+        for path in self.get_option('additional_expectations', []):
+            expanded_path = self._filesystem.expanduser(path)
+            if self._filesystem.exists(expanded_path):
+                _log.debug("reading additional_expectations from path '%s'" % path)
+                expectations[path] = self._filesystem.read_text_file(expanded_path)
+            else:
+                _log.warning("additional_expectations path '%s' does not exist" % path)
+        return expectations
+
+    def expectations_files(self):
+        # Unlike baseline_search_path, we only want to search [WK2-PORT, PORT-VERSION, PORT] and any directories
+        # included via --additional-platform-directory, not the full casade.
+        search_paths = [self.port_name]
+        if self.name() != self.port_name:
+            search_paths.append(self.name())
+
+        if self.get_option('webkit_test_runner'):
+            # Because nearly all of the skipped tests for WebKit 2 are due to cross-platform
+            # issues, all wk2 ports share a skipped list under platform/wk2.
+            search_paths.extend([self._wk2_port_name(), "wk2"])
+
+        search_paths.extend(self.get_option("additional_platform_directory", []))
+
+        return [self._filesystem.join(self._webkit_baseline_path(d), 'TestExpectations') for d in search_paths]
+
+    def repository_paths(self):
+        """Returns a list of (repository_name, repository_path) tuples of its depending code base.
+        By default it returns a list that only contains a ('webkit', <webkitRepossitoryPath>) tuple."""
+
+        # We use LayoutTest directory here because webkit_base isn't a part webkit repository in Chromium port
+        # where turnk isn't checked out as a whole.
+        return [('webkit', self.layout_tests_dir())]
+
+    _WDIFF_DEL = '##WDIFF_DEL##'
+    _WDIFF_ADD = '##WDIFF_ADD##'
+    _WDIFF_END = '##WDIFF_END##'
+
+    def _format_wdiff_output_as_html(self, wdiff):
+        wdiff = cgi.escape(wdiff)
+        wdiff = wdiff.replace(self._WDIFF_DEL, "<span class=del>")
+        wdiff = wdiff.replace(self._WDIFF_ADD, "<span class=add>")
+        wdiff = wdiff.replace(self._WDIFF_END, "</span>")
+        html = "<head><style>.del { background: #faa; } "
+        html += ".add { background: #afa; }</style></head>"
+        html += "<pre>%s</pre>" % wdiff
+        return html
+
+    def _wdiff_command(self, actual_filename, expected_filename):
+        executable = self._path_to_wdiff()
+        return [executable,
+                "--start-delete=%s" % self._WDIFF_DEL,
+                "--end-delete=%s" % self._WDIFF_END,
+                "--start-insert=%s" % self._WDIFF_ADD,
+                "--end-insert=%s" % self._WDIFF_END,
+                actual_filename,
+                expected_filename]
+
+    @staticmethod
+    def _handle_wdiff_error(script_error):
+        # Exit 1 means the files differed, any other exit code is an error.
+        if script_error.exit_code != 1:
+            raise script_error
+
+    def _run_wdiff(self, actual_filename, expected_filename):
+        """Runs wdiff and may throw exceptions.
+        This is mostly a hook for unit testing."""
+        # Diffs are treated as binary as they may include multiple files
+        # with conflicting encodings.  Thus we do not decode the output.
+        command = self._wdiff_command(actual_filename, expected_filename)
+        wdiff = self._executive.run_command(command, decode_output=False,
+            error_handler=self._handle_wdiff_error)
+        return self._format_wdiff_output_as_html(wdiff)
+
+    def wdiff_text(self, actual_filename, expected_filename):
+        """Returns a string of HTML indicating the word-level diff of the
+        contents of the two filenames. Returns an empty string if word-level
+        diffing isn't available."""
+        if not self.wdiff_available():
+            return ""
+        try:
+            # It's possible to raise a ScriptError we pass wdiff invalid paths.
+            return self._run_wdiff(actual_filename, expected_filename)
+        except OSError, e:
+            if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]:
+                # Silently ignore cases where wdiff is missing.
+                self._wdiff_available = False
+                return ""
+            raise
+
+    # 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):
+        if self._pretty_patch_available is None:
+            self._pretty_patch_available = self.check_pretty_patch(logging=False)
+        if not self._pretty_patch_available:
+            return self._pretty_patch_error_html
+        command = ("ruby", "-I", self._filesystem.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.
+            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.
+            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 default_configuration(self):
+        return self._config.default_configuration()
+
+    #
+    # PROTECTED ROUTINES
+    #
+    # The routines below should only be called by routines in this class
+    # or any of its subclasses.
+    #
+
+    def _uses_apache(self):
+        return True
+
+    # FIXME: This does not belong on the port object.
+    @memoized
+    def _path_to_apache(self):
+        """Returns the full path to the apache binary.
+
+        This is needed only by ports that use the apache_http_server module."""
+        # The Apache binary path can vary depending on OS and distribution
+        # See http://wiki.apache.org/httpd/DistrosDefaultLayout
+        for path in ["/usr/sbin/httpd", "/usr/sbin/apache2"]:
+            if self._filesystem.exists(path):
+                return path
+        _log.error("Could not find apache. Not installed or unknown path.")
+        return None
+
+    # FIXME: This belongs on some platform abstraction instead of Port.
+    def _is_redhat_based(self):
+        return self._filesystem.exists('/etc/redhat-release')
+
+    def _is_debian_based(self):
+        return self._filesystem.exists('/etc/debian_version')
+
+    # We pass sys_platform into this method to make it easy to unit test.
+    def _apache_config_file_name_for_platform(self, sys_platform):
+        if sys_platform == 'cygwin':
+            return 'cygwin-httpd.conf'  # CYGWIN is the only platform to still use Apache 1.3.
+        if sys_platform.startswith('linux'):
+            if self._is_redhat_based():
+                return 'fedora-httpd.conf'  # This is an Apache 2.x config file despite the naming.
+            if self._is_debian_based():
+                return 'apache2-debian-httpd.conf'
+        # All platforms use apache2 except for CYGWIN (and Mac OS X Tiger and prior, which we no longer support).
+        return "apache2-httpd.conf"
+
+    def _path_to_apache_config_file(self):
+        """Returns the full path to the apache configuration file.
+
+        If the WEBKIT_HTTP_SERVER_CONF_PATH environment variable is set, its
+        contents will be used instead.
+
+        This is needed only by ports that use the apache_http_server module."""
+        config_file_from_env = os.environ.get('WEBKIT_HTTP_SERVER_CONF_PATH')
+        if config_file_from_env:
+            if not self._filesystem.exists(config_file_from_env):
+                raise IOError('%s was not found on the system' % config_file_from_env)
+            return config_file_from_env
+
+        config_file_name = self._apache_config_file_name_for_platform(sys.platform)
+        return self._filesystem.join(self.layout_tests_dir(), 'http', 'conf', config_file_name)
+
+    def _build_path(self, *comps):
+        root_directory = self.get_option('root')
+        if not root_directory:
+            build_directory = self.get_option('build_directory')
+            if build_directory:
+                root_directory = self._filesystem.join(build_directory, self.get_option('configuration'))
+            else:
+                root_directory = self._config.build_directory(self.get_option('configuration'))
+            # Set --root so that we can pass this to subprocesses and avoid making the
+            # slow call to config.build_directory() N times in each worker.
+            # FIXME: This is like @memoized, but more annoying and fragile; there should be another
+            # way to propagate values without mutating the options list.
+            self.set_option_default('root', root_directory)
+        return self._filesystem.join(self._filesystem.abspath(root_directory), *comps)
+
+    def _path_to_driver(self, configuration=None):
+        """Returns the full path to the test driver (DumpRenderTree)."""
+        return self._build_path(self.driver_name())
+
+    def _path_to_webcore_library(self):
+        """Returns the full path to a built copy of WebCore."""
+        return None
+
+    def _path_to_helper(self):
+        """Returns the full path to the layout_test_helper binary, which
+        is used to help configure the system for the test run, or None
+        if no helper is needed.
+
+        This is likely only used by start/stop_helper()."""
+        return None
+
+    def _path_to_image_diff(self):
+        """Returns the full path to the image_diff binary, or None if it is not available.
+
+        This is likely used only by diff_image()"""
+        return self._build_path('ImageDiff')
+
+    def _path_to_lighttpd(self):
+        """Returns the path to the LigHTTPd binary.
+
+        This is needed only by ports that use the http_server.py module."""
+        raise NotImplementedError('Port._path_to_lighttpd')
+
+    def _path_to_lighttpd_modules(self):
+        """Returns the path to the LigHTTPd modules directory.
+
+        This is needed only by ports that use the http_server.py module."""
+        raise NotImplementedError('Port._path_to_lighttpd_modules')
+
+    def _path_to_lighttpd_php(self):
+        """Returns the path to the LigHTTPd PHP executable.
+
+        This is needed only by ports that use the http_server.py module."""
+        raise NotImplementedError('Port._path_to_lighttpd_php')
+
+    @memoized
+    def _path_to_wdiff(self):
+        """Returns the full path to the wdiff binary, or None if it is not available.
+
+        This is likely used only by wdiff_text()"""
+        for path in ("/usr/bin/wdiff", "/usr/bin/dwdiff"):
+            if self._filesystem.exists(path):
+                return path
+        return None
+
+    def _webkit_baseline_path(self, platform):
+        """Return the  full path to the top of the baseline tree for a
+        given platform."""
+        return self._filesystem.join(self.layout_tests_dir(), 'platform', platform)
+
+    # FIXME: Belongs on a Platform object.
+    def _generate_all_test_configurations(self):
+        """Generates a list of TestConfiguration instances, representing configurations
+        for a platform across all OSes, architectures, build and graphics types."""
+        raise NotImplementedError('Port._generate_test_configurations')
+
+    def _driver_class(self):
+        """Returns the port's driver implementation."""
+        return driver.Driver
+
+    def _get_crash_log(self, name, pid, stdout, stderr, newer_than):
+        name_str = name or '<unknown process name>'
+        pid_str = str(pid or '<unknown>')
+        stdout_lines = (stdout or '<empty>').decode('utf8', 'replace').splitlines()
+        stderr_lines = (stderr or '<empty>').decode('utf8', 'replace').splitlines()
+        return (stderr, 'crash log for %s (pid %s):\n%s\n%s\n' % (name_str, pid_str,
+            '\n'.join(('STDOUT: ' + l) for l in stdout_lines),
+            '\n'.join(('STDERR: ' + l) for l in stderr_lines)))
+
+    def look_for_new_crash_logs(self, crashed_processes, start_time):
+        pass
+
+    def sample_process(self, name, pid):
+        pass
+
+    def virtual_test_suites(self):
+        return []
+
+    @memoized
+    def populated_virtual_test_suites(self):
+        suites = self.virtual_test_suites()
+
+        # Sanity-check the suites to make sure they don't point to other suites.
+        suite_dirs = [suite.name for suite in suites]
+        for suite in suites:
+            assert suite.base not in suite_dirs
+
+        for suite in suites:
+            base_tests = self._real_tests([suite.base])
+            suite.tests = {}
+            for test in base_tests:
+                suite.tests[test.replace(suite.base, suite.name, 1)] = test
+        return suites
+
+    def _virtual_tests(self, paths, suites):
+        virtual_tests = set()
+        for suite in suites:
+            if paths:
+                for test in suite.tests:
+                    if any(test.startswith(p) for p in paths):
+                        virtual_tests.add(test)
+            else:
+                virtual_tests.update(set(suite.tests.keys()))
+        return virtual_tests
+
+    def lookup_virtual_test_base(self, test_name):
+        for suite in self.populated_virtual_test_suites():
+            if test_name.startswith(suite.name):
+                return test_name.replace(suite.name, suite.base, 1)
+        return None
+
+    def lookup_virtual_test_args(self, test_name):
+        for suite in self.populated_virtual_test_suites():
+            if test_name.startswith(suite.name):
+                return suite.args
+        return []
+
+    def should_run_as_pixel_test(self, test_input):
+        if not self._options.pixel_tests:
+            return False
+        if self._options.pixel_test_directories:
+            return any(test_input.test_name.startswith(directory) for directory in self._options.pixel_test_directories)
+        return self._should_run_as_pixel_test(test_input)
+
+    def _should_run_as_pixel_test(self, test_input):
+        # Default behavior is to allow all test to run as pixel tests if --pixel-tests is on and
+        # --pixel-test-directory is not specified.
+        return True
+
+    # FIXME: Eventually we should standarize port naming, and make this method smart enough
+    # to use for all port configurations (including architectures, graphics types, etc).
+    def _port_flag_for_scripts(self):
+        # This is overrriden by ports which need a flag passed to scripts to distinguish the use of that port.
+        # For example --qt on linux, since a user might have both Gtk and Qt libraries installed.
+        # FIXME: Chromium should override this once ChromiumPort is a WebKitPort.
+        return None
+
+    # This is modeled after webkitdirs.pm argumentsForConfiguration() from old-run-webkit-tests
+    def _arguments_for_configuration(self):
+        config_args = []
+        config_args.append(self._config.flag_for_configuration(self.get_option('configuration')))
+        # FIXME: We may need to add support for passing --32-bit like old-run-webkit-tests had.
+        port_flag = self._port_flag_for_scripts()
+        if port_flag:
+            config_args.append(port_flag)
+        return config_args
+
+    def _run_script(self, script_name, args=None, include_configuration_arguments=True, decode_output=True, env=None):
+        run_script_command = [self._config.script_path(script_name)]
+        if include_configuration_arguments:
+            run_script_command.extend(self._arguments_for_configuration())
+        if args:
+            run_script_command.extend(args)
+        output = self._executive.run_command(run_script_command, cwd=self._config.webkit_base_dir(), decode_output=decode_output, env=env)
+        _log.debug('Output of %s:\n%s' % (run_script_command, output))
+        return output
+
+    def _build_driver(self):
+        environment = self.host.copy_current_environment()
+        environment.disable_gcc_smartquotes()
+        env = environment.to_dictionary()
+
+        # FIXME: We build both DumpRenderTree and WebKitTestRunner for
+        # WebKitTestRunner runs because DumpRenderTree still includes
+        # the DumpRenderTreeSupport module and the TestNetscapePlugin.
+        # These two projects should be factored out into their own
+        # projects.
+        try:
+            self._run_script("build-dumprendertree", args=self._build_driver_flags(), env=env)
+            if self.get_option('webkit_test_runner'):
+                self._run_script("build-webkittestrunner", args=self._build_driver_flags(), env=env)
+        except ScriptError, e:
+            _log.error(e.message_with_output(output_limit=None))
+            return False
+        return True
+
+    def _build_driver_flags(self):
+        return []
+
+    def _tests_for_other_platforms(self):
+        # By default we will skip any directory under LayoutTests/platform
+        # that isn't in our baseline search path (this mirrors what
+        # old-run-webkit-tests does in findTestsToRun()).
+        # Note this returns LayoutTests/platform/*, not platform/*/*.
+        entries = self._filesystem.glob(self._webkit_baseline_path('*'))
+        dirs_to_skip = []
+        for entry in entries:
+            if self._filesystem.isdir(entry) and entry not in self.baseline_search_path():
+                basename = self._filesystem.basename(entry)
+                dirs_to_skip.append('platform/%s' % basename)
+        return dirs_to_skip
+
+    def _runtime_feature_list(self):
+        """If a port makes certain features available only through runtime flags, it can override this routine to indicate which ones are available."""
+        return None
+
+    def nm_command(self):
+        return 'nm'
+
+    def _modules_to_search_for_symbols(self):
+        path = self._path_to_webcore_library()
+        if path:
+            return [path]
+        return []
+
+    def _symbols_string(self):
+        symbols = ''
+        for path_to_module in self._modules_to_search_for_symbols():
+            try:
+                symbols += self._executive.run_command([self.nm_command(), path_to_module], error_handler=self._executive.ignore_error)
+            except OSError, e:
+                _log.warn("Failed to run nm: %s.  Can't determine supported features correctly." % e)
+        return symbols
+
+    # Ports which use run-time feature detection should define this method and return
+    # a dictionary mapping from Feature Names to skipped directoires.  NRWT will
+    # run DumpRenderTree --print-supported-features and parse the output.
+    # If the Feature Names are not found in the output, the corresponding directories
+    # will be skipped.
+    def _missing_feature_to_skipped_tests(self):
+        """Return the supported feature dictionary. Keys are feature names and values
+        are the lists of directories to skip if the feature name is not matched."""
+        # FIXME: This list matches WebKitWin and should be moved onto the Win port.
+        return {
+            "Accelerated Compositing": ["compositing"],
+            "3D Rendering": ["animations/3d", "transforms/3d"],
+        }
+
+    # Ports which use compile-time feature detection should define this method and return
+    # a dictionary mapping from symbol substrings to possibly disabled test directories.
+    # When the symbol substrings are not matched, the directories will be skipped.
+    # If ports don't ever enable certain features, then those directories can just be
+    # in the Skipped list instead of compile-time-checked here.
+    def _missing_symbol_to_skipped_tests(self):
+        """Return the supported feature dictionary. The keys are symbol-substrings
+        and the values are the lists of directories to skip if that symbol is missing."""
+        return {
+            "MathMLElement": ["mathml"],
+            "GraphicsLayer": ["compositing"],
+            "WebCoreHas3DRendering": ["animations/3d", "transforms/3d"],
+            "WebGLShader": ["fast/canvas/webgl", "compositing/webgl", "http/tests/canvas/webgl"],
+            "MHTMLArchive": ["mhtml"],
+            "CSSVariableValue": ["fast/css/variables", "inspector/styles/variables"],
+        }
+
+    def _has_test_in_directories(self, directory_lists, test_list):
+        if not test_list:
+            return False
+
+        directories = itertools.chain.from_iterable(directory_lists)
+        for directory, test in itertools.product(directories, test_list):
+            if test.startswith(directory):
+                return True
+        return False
+
+    def _skipped_tests_for_unsupported_features(self, test_list):
+        # Only check the runtime feature list of there are tests in the test_list that might get skipped.
+        # This is a performance optimization to avoid the subprocess call to DRT.
+        # If the port supports runtime feature detection, disable any tests
+        # for features missing from the runtime feature list.
+        # If _runtime_feature_list returns a non-None value, then prefer
+        # runtime feature detection over static feature detection.
+        if self._has_test_in_directories(self._missing_feature_to_skipped_tests().values(), test_list):
+            supported_feature_list = self._runtime_feature_list()
+            if supported_feature_list is not None:
+                return reduce(operator.add, [directories for feature, directories in self._missing_feature_to_skipped_tests().items() if feature not in supported_feature_list])
+
+        # Only check the symbols of there are tests in the test_list that might get skipped.
+        # This is a performance optimization to avoid the calling nm.
+        # Runtime feature detection not supported, fallback to static dectection:
+        # Disable any tests for symbols missing from the executable or libraries.
+        if self._has_test_in_directories(self._missing_symbol_to_skipped_tests().values(), test_list):
+            symbols_string = self._symbols_string()
+            if symbols_string is not None:
+                return reduce(operator.add, [directories for symbol_substring, directories in self._missing_symbol_to_skipped_tests().items() if symbol_substring not in symbols_string], [])
+
+        return []
+
+    def _wk2_port_name(self):
+        # By current convention, the WebKit2 name is always mac-wk2, win-wk2, not mac-leopard-wk2, etc,
+        # except for Qt because WebKit2 is only supported by Qt 5.0 (therefore: qt-5.0-wk2).
+        return "%s-wk2" % self.port_name
+
+
+class VirtualTestSuite(object):
+    def __init__(self, name, base, args, tests=None):
+        self.name = name
+        self.base = base
+        self.args = args
+        self.tests = tests or set()
+
+    def __repr__(self):
+        return "VirtualTestSuite('%s', '%s', %s)" % (self.name, self.base, self.args)
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/base_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/base_unittest.py
new file mode 100644
index 0000000..1fe75cc
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/base_unittest.py
@@ -0,0 +1,467 @@
+# 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 logging
+import optparse
+import sys
+import tempfile
+import unittest
+
+from webkitpy.common.system.executive import Executive, ScriptError
+from webkitpy.common.system import executive_mock
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.system.path import abspath_to_uri
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.mocktool import MockOptions
+from webkitpy.common.system.executive_mock import MockExecutive, MockExecutive2
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+
+from webkitpy.layout_tests.port import Port, Driver, DriverOutput
+from webkitpy.layout_tests.port.test import add_unit_tests_to_mock_filesystem, TestPort
+
+import config
+import config_mock
+
+class PortTest(unittest.TestCase):
+    def make_port(self, executive=None, with_tests=False, **kwargs):
+        host = MockSystemHost()
+        if executive:
+            host.executive = executive
+        if with_tests:
+            add_unit_tests_to_mock_filesystem(host.filesystem)
+            return TestPort(host, **kwargs)
+        return Port(host, **kwargs)
+
+    def test_default_child_processes(self):
+        port = self.make_port()
+        self.assertNotEquals(port.default_child_processes(), None)
+
+    def test_format_wdiff_output_as_html(self):
+        output = "OUTPUT %s %s %s" % (Port._WDIFF_DEL, Port._WDIFF_ADD, Port._WDIFF_END)
+        html = self.make_port()._format_wdiff_output_as_html(output)
+        expected_html = "<head><style>.del { background: #faa; } .add { background: #afa; }</style></head><pre>OUTPUT <span class=del> <span class=add> </span></pre>"
+        self.assertEqual(html, expected_html)
+
+    def test_wdiff_command(self):
+        port = self.make_port()
+        port._path_to_wdiff = lambda: "/path/to/wdiff"
+        command = port._wdiff_command("/actual/path", "/expected/path")
+        expected_command = [
+            "/path/to/wdiff",
+            "--start-delete=##WDIFF_DEL##",
+            "--end-delete=##WDIFF_END##",
+            "--start-insert=##WDIFF_ADD##",
+            "--end-insert=##WDIFF_END##",
+            "/actual/path",
+            "/expected/path",
+        ]
+        self.assertEqual(command, expected_command)
+
+    def _file_with_contents(self, contents, encoding="utf-8"):
+        new_file = tempfile.NamedTemporaryFile()
+        new_file.write(contents.encode(encoding))
+        new_file.flush()
+        return new_file
+
+    def test_pretty_patch_os_error(self):
+        port = self.make_port(executive=executive_mock.MockExecutive2(exception=OSError))
+        oc = OutputCapture()
+        oc.capture_output()
+        self.assertEqual(port.pretty_patch_text("patch.txt"),
+                         port._pretty_patch_error_html)
+
+        # This tests repeated calls to make sure we cache the result.
+        self.assertEqual(port.pretty_patch_text("patch.txt"),
+                         port._pretty_patch_error_html)
+        oc.restore_output()
+
+    def test_pretty_patch_script_error(self):
+        # FIXME: This is some ugly white-box test hacking ...
+        port = self.make_port(executive=executive_mock.MockExecutive2(exception=ScriptError))
+        port._pretty_patch_available = True
+        self.assertEqual(port.pretty_patch_text("patch.txt"),
+                         port._pretty_patch_error_html)
+
+        # This tests repeated calls to make sure we cache the result.
+        self.assertEqual(port.pretty_patch_text("patch.txt"),
+                         port._pretty_patch_error_html)
+
+    def integration_test_run_wdiff(self):
+        executive = Executive()
+        # This may fail on some systems.  We could ask the port
+        # object for the wdiff path, but since we don't know what
+        # port object to use, this is sufficient for now.
+        try:
+            wdiff_path = executive.run_command(["which", "wdiff"]).rstrip()
+        except Exception, e:
+            wdiff_path = None
+
+        port = self.make_port(executive=executive)
+        port._path_to_wdiff = lambda: wdiff_path
+
+        if wdiff_path:
+            # "with tempfile.NamedTemporaryFile() as actual" does not seem to work in Python 2.5
+            actual = self._file_with_contents(u"foo")
+            expected = self._file_with_contents(u"bar")
+            wdiff = port._run_wdiff(actual.name, expected.name)
+            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.
+            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(port._wdiff_available)
+            actual.close()
+            expected.close()
+
+            # Bogus paths should raise a script error.
+            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(port._wdiff_available)
+
+        # If wdiff does not exist _run_wdiff should throw an OSError.
+        port._path_to_wdiff = lambda: "/invalid/path/to/wdiff"
+        self.assertRaises(OSError, port._run_wdiff, "foo", "bar")
+
+        # 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(port._wdiff_available)
+
+    def test_wdiff_text(self):
+        port = self.make_port()
+        port.wdiff_available = lambda: True
+        port._run_wdiff = lambda a, b: 'PASS'
+        self.assertEqual('PASS', port.wdiff_text(None, None))
+
+    def test_diff_text(self):
+        port = self.make_port()
+        # Make sure that we don't run into decoding exceptions when the
+        # filenames are unicode, with regular or malformed input (expected or
+        # actual input is always raw bytes, not unicode).
+        port.diff_text('exp', 'act', 'exp.txt', 'act.txt')
+        port.diff_text('exp', 'act', u'exp.txt', 'act.txt')
+        port.diff_text('exp', 'act', u'a\xac\u1234\u20ac\U00008000', 'act.txt')
+
+        port.diff_text('exp' + chr(255), 'act', 'exp.txt', 'act.txt')
+        port.diff_text('exp' + chr(255), 'act', u'exp.txt', 'act.txt')
+
+        # Though expected and actual files should always be read in with no
+        # encoding (and be stored as str objects), test unicode inputs just to
+        # be safe.
+        port.diff_text(u'exp', 'act', 'exp.txt', 'act.txt')
+        port.diff_text(
+            u'a\xac\u1234\u20ac\U00008000', 'act', 'exp.txt', 'act.txt')
+
+        # And make sure we actually get diff output.
+        diff = port.diff_text('foo', 'bar', 'exp.txt', 'act.txt')
+        self.assertTrue('foo' in diff)
+        self.assertTrue('bar' in diff)
+        self.assertTrue('exp.txt' in diff)
+        self.assertTrue('act.txt' in diff)
+        self.assertFalse('nosuchthing' in diff)
+
+    def test_default_configuration_notfound(self):
+        # Test that we delegate to the config object properly.
+        port = self.make_port(config=config_mock.MockConfig(default_configuration='default'))
+        self.assertEqual(port.default_configuration(), 'default')
+
+    def test_setup_test_run(self):
+        port = self.make_port()
+        # This routine is a no-op. We just test it for coverage.
+        port.setup_test_run()
+
+    def test_test_dirs(self):
+        port = self.make_port()
+        port.host.filesystem.write_text_file(port.layout_tests_dir() + '/canvas/test', '')
+        port.host.filesystem.write_text_file(port.layout_tests_dir() + '/css2.1/test', '')
+        dirs = port.test_dirs()
+        self.assertTrue('canvas' in dirs)
+        self.assertTrue('css2.1' in dirs)
+
+    def test_skipped_perf_tests(self):
+        port = self.make_port()
+
+        def add_text_file(dirname, filename, content='some content'):
+            dirname = port.host.filesystem.join(port.perf_tests_dir(), dirname)
+            port.host.filesystem.maybe_make_directory(dirname)
+            port.host.filesystem.write_text_file(port.host.filesystem.join(dirname, filename), content)
+
+        add_text_file('inspector', 'test1.html')
+        add_text_file('inspector', 'unsupported_test1.html')
+        add_text_file('inspector', 'test2.html')
+        add_text_file('inspector/resources', 'resource_file.html')
+        add_text_file('unsupported', 'unsupported_test2.html')
+        add_text_file('', 'Skipped', '\n'.join(['Layout', '', 'SunSpider', 'Supported/some-test.html']))
+        self.assertEqual(port.skipped_perf_tests(), ['Layout', 'SunSpider', 'Supported/some-test.html'])
+
+    def test_get_option__set(self):
+        options, args = optparse.OptionParser().parse_args([])
+        options.foo = 'bar'
+        port = self.make_port(options=options)
+        self.assertEqual(port.get_option('foo'), 'bar')
+
+    def test_get_option__unset(self):
+        port = self.make_port()
+        self.assertEqual(port.get_option('foo'), None)
+
+    def test_get_option__default(self):
+        port = self.make_port()
+        self.assertEqual(port.get_option('foo', 'bar'), 'bar')
+
+    def test_additional_platform_directory(self):
+        port = self.make_port(port_name='foo')
+        port.default_baseline_search_path = lambda: ['LayoutTests/platform/foo']
+        layout_test_dir = port.layout_tests_dir()
+        test_file = 'fast/test.html'
+
+        # No additional platform directory
+        self.assertEqual(
+            port.expected_baselines(test_file, '.txt'),
+            [(None, 'fast/test-expected.txt')])
+        self.assertEqual(port.baseline_path(), 'LayoutTests/platform/foo')
+
+        # Simple additional platform directory
+        port._options.additional_platform_directory = ['/tmp/local-baselines']
+        port._filesystem.write_text_file('/tmp/local-baselines/fast/test-expected.txt', 'foo')
+        self.assertEqual(
+            port.expected_baselines(test_file, '.txt'),
+            [('/tmp/local-baselines', 'fast/test-expected.txt')])
+        self.assertEqual(port.baseline_path(), '/tmp/local-baselines')
+
+        # Multiple additional platform directories
+        port._options.additional_platform_directory = ['/foo', '/tmp/local-baselines']
+        self.assertEqual(
+            port.expected_baselines(test_file, '.txt'),
+            [('/tmp/local-baselines', 'fast/test-expected.txt')])
+        self.assertEqual(port.baseline_path(), '/foo')
+
+    def test_nonexistant_expectations(self):
+        port = self.make_port(port_name='foo')
+        port.expectations_files = lambda: ['/mock-checkout/LayoutTests/platform/exists/TestExpectations', '/mock-checkout/LayoutTests/platform/nonexistant/TestExpectations']
+        port._filesystem.write_text_file('/mock-checkout/LayoutTests/platform/exists/TestExpectations', '')
+        self.assertEquals('\n'.join(port.expectations_dict().keys()), '/mock-checkout/LayoutTests/platform/exists/TestExpectations')
+
+    def test_additional_expectations(self):
+        port = self.make_port(port_name='foo')
+        port.port_name = 'foo'
+        port._filesystem.write_text_file('/mock-checkout/LayoutTests/platform/foo/TestExpectations', '')
+        port._filesystem.write_text_file(
+            '/tmp/additional-expectations-1.txt', 'content1\n')
+        port._filesystem.write_text_file(
+            '/tmp/additional-expectations-2.txt', 'content2\n')
+
+        self.assertEquals('\n'.join(port.expectations_dict().values()), '')
+
+        port._options.additional_expectations = [
+            '/tmp/additional-expectations-1.txt']
+        self.assertEquals('\n'.join(port.expectations_dict().values()), '\ncontent1\n')
+
+        port._options.additional_expectations = [
+            '/tmp/nonexistent-file', '/tmp/additional-expectations-1.txt']
+        self.assertEquals('\n'.join(port.expectations_dict().values()), '\ncontent1\n')
+
+        port._options.additional_expectations = [
+            '/tmp/additional-expectations-1.txt', '/tmp/additional-expectations-2.txt']
+        self.assertEquals('\n'.join(port.expectations_dict().values()), '\ncontent1\n\ncontent2\n')
+
+    def test_additional_env_var(self):
+        port = self.make_port(options=optparse.Values({'additional_env_var': ['FOO=BAR', 'BAR=FOO']}))
+        self.assertEqual(port.get_option('additional_env_var'), ['FOO=BAR', 'BAR=FOO'])
+        environment = port.setup_environ_for_server()
+        self.assertTrue(('FOO' in environment) & ('BAR' in environment))
+        self.assertEqual(environment['FOO'], 'BAR')
+        self.assertEqual(environment['BAR'], 'FOO')
+
+    def test_uses_test_expectations_file(self):
+        port = self.make_port(port_name='foo')
+        port.port_name = 'foo'
+        port.path_to_test_expectations_file = lambda: '/mock-results/TestExpectations'
+        self.assertFalse(port.uses_test_expectations_file())
+        port._filesystem = MockFileSystem({'/mock-results/TestExpectations': ''})
+        self.assertTrue(port.uses_test_expectations_file())
+
+    def test_find_no_paths_specified(self):
+        port = self.make_port(with_tests=True)
+        layout_tests_dir = port.layout_tests_dir()
+        tests = port.tests([])
+        self.assertNotEqual(len(tests), 0)
+
+    def test_find_one_test(self):
+        port = self.make_port(with_tests=True)
+        tests = port.tests(['failures/expected/image.html'])
+        self.assertEqual(len(tests), 1)
+
+    def test_find_glob(self):
+        port = self.make_port(with_tests=True)
+        tests = port.tests(['failures/expected/im*'])
+        self.assertEqual(len(tests), 2)
+
+    def test_find_with_skipped_directories(self):
+        port = self.make_port(with_tests=True)
+        tests = port.tests(['userscripts'])
+        self.assertTrue('userscripts/resources/iframe.html' not in tests)
+
+    def test_find_with_skipped_directories_2(self):
+        port = self.make_port(with_tests=True)
+        tests = port.tests(['userscripts/resources'])
+        self.assertEqual(tests, set([]))
+
+    def test_is_test_file(self):
+        filesystem = MockFileSystem()
+        self.assertTrue(Port._is_test_file(filesystem, '', 'foo.html'))
+        self.assertTrue(Port._is_test_file(filesystem, '', 'foo.shtml'))
+        self.assertTrue(Port._is_test_file(filesystem, '', 'foo.svg'))
+        self.assertTrue(Port._is_test_file(filesystem, '', 'test-ref-test.html'))
+        self.assertFalse(Port._is_test_file(filesystem, '', 'foo.png'))
+        self.assertFalse(Port._is_test_file(filesystem, '', 'foo-expected.html'))
+        self.assertFalse(Port._is_test_file(filesystem, '', 'foo-expected.svg'))
+        self.assertFalse(Port._is_test_file(filesystem, '', 'foo-expected.xht'))
+        self.assertFalse(Port._is_test_file(filesystem, '', 'foo-expected-mismatch.html'))
+        self.assertFalse(Port._is_test_file(filesystem, '', 'foo-expected-mismatch.svg'))
+        self.assertFalse(Port._is_test_file(filesystem, '', 'foo-expected-mismatch.xhtml'))
+        self.assertFalse(Port._is_test_file(filesystem, '', 'foo-ref.html'))
+        self.assertFalse(Port._is_test_file(filesystem, '', 'foo-notref.html'))
+        self.assertFalse(Port._is_test_file(filesystem, '', 'foo-notref.xht'))
+        self.assertFalse(Port._is_test_file(filesystem, '', 'foo-ref.xhtml'))
+        self.assertFalse(Port._is_test_file(filesystem, '', 'ref-foo.html'))
+        self.assertFalse(Port._is_test_file(filesystem, '', 'notref-foo.xhr'))
+
+    def test_parse_reftest_list(self):
+        port = self.make_port(with_tests=True)
+        port.host.filesystem.files['bar/reftest.list'] = "\n".join(["== test.html test-ref.html",
+        "",
+        "# some comment",
+        "!= test-2.html test-notref.html # more comments",
+        "== test-3.html test-ref.html",
+        "== test-3.html test-ref2.html",
+        "!= test-3.html test-notref.html"])
+
+        reftest_list = Port._parse_reftest_list(port.host.filesystem, 'bar')
+        self.assertEqual(reftest_list, {'bar/test.html': [('==', 'bar/test-ref.html')],
+            'bar/test-2.html': [('!=', 'bar/test-notref.html')],
+            'bar/test-3.html': [('==', 'bar/test-ref.html'), ('==', 'bar/test-ref2.html'), ('!=', 'bar/test-notref.html')]})
+
+    def test_reference_files(self):
+        port = self.make_port(with_tests=True)
+        self.assertEqual(port.reference_files('passes/svgreftest.svg'), [('==', port.layout_tests_dir() + '/passes/svgreftest-expected.svg')])
+        self.assertEqual(port.reference_files('passes/xhtreftest.svg'), [('==', port.layout_tests_dir() + '/passes/xhtreftest-expected.html')])
+        self.assertEqual(port.reference_files('passes/phpreftest.php'), [('!=', port.layout_tests_dir() + '/passes/phpreftest-expected-mismatch.svg')])
+
+    def test_operating_system(self):
+        self.assertEqual('mac', self.make_port().operating_system())
+
+    def test_http_server_supports_ipv6(self):
+        port = self.make_port()
+        self.assertTrue(port.http_server_supports_ipv6())
+        port.host.platform.os_name = 'cygwin'
+        self.assertFalse(port.http_server_supports_ipv6())
+        port.host.platform.os_name = 'win'
+        self.assertTrue(port.http_server_supports_ipv6())
+
+    def test_check_httpd_success(self):
+        port = self.make_port(executive=MockExecutive2())
+        port._path_to_apache = lambda: '/usr/sbin/httpd'
+        capture = OutputCapture()
+        capture.capture_output()
+        self.assertTrue(port.check_httpd())
+        _, _, logs = capture.restore_output()
+        self.assertEqual('', logs)
+
+    def test_httpd_returns_error_code(self):
+        port = self.make_port(executive=MockExecutive2(exit_code=1))
+        port._path_to_apache = lambda: '/usr/sbin/httpd'
+        capture = OutputCapture()
+        capture.capture_output()
+        self.assertFalse(port.check_httpd())
+        _, _, logs = capture.restore_output()
+        self.assertEqual('httpd seems broken. Cannot run http tests.\n', logs)
+
+    def test_test_exists(self):
+        port = self.make_port(with_tests=True)
+        self.assertTrue(port.test_exists('passes'))
+        self.assertTrue(port.test_exists('passes/text.html'))
+        self.assertFalse(port.test_exists('passes/does_not_exist.html'))
+
+        self.assertTrue(port.test_exists('virtual'))
+        self.assertFalse(port.test_exists('virtual/does_not_exist.html'))
+        self.assertTrue(port.test_exists('virtual/passes/text.html'))
+
+    def test_test_isfile(self):
+        port = self.make_port(with_tests=True)
+        self.assertFalse(port.test_isfile('passes'))
+        self.assertTrue(port.test_isfile('passes/text.html'))
+        self.assertFalse(port.test_isfile('passes/does_not_exist.html'))
+
+        self.assertFalse(port.test_isfile('virtual'))
+        self.assertTrue(port.test_isfile('virtual/passes/text.html'))
+        self.assertFalse(port.test_isfile('virtual/does_not_exist.html'))
+
+    def test_test_isdir(self):
+        port = self.make_port(with_tests=True)
+        self.assertTrue(port.test_isdir('passes'))
+        self.assertFalse(port.test_isdir('passes/text.html'))
+        self.assertFalse(port.test_isdir('passes/does_not_exist.html'))
+        self.assertFalse(port.test_isdir('passes/does_not_exist/'))
+
+        self.assertTrue(port.test_isdir('virtual'))
+        self.assertFalse(port.test_isdir('virtual/does_not_exist.html'))
+        self.assertFalse(port.test_isdir('virtual/does_not_exist/'))
+        self.assertFalse(port.test_isdir('virtual/passes/text.html'))
+
+    def test_tests(self):
+        port = self.make_port(with_tests=True)
+        tests = port.tests([])
+        self.assertTrue('passes/text.html' in tests)
+        self.assertTrue('virtual/passes/text.html' in tests)
+
+        tests = port.tests(['passes'])
+        self.assertTrue('passes/text.html' in tests)
+        self.assertTrue('passes/passes/test-virtual-passes.html' in tests)
+        self.assertFalse('virtual/passes/text.html' in tests)
+
+        tests = port.tests(['virtual/passes'])
+        self.assertFalse('passes/text.html' in tests)
+        self.assertTrue('virtual/passes/test-virtual-passes.html' in tests)
+        self.assertTrue('virtual/passes/passes/test-virtual-passes.html' in tests)
+        self.assertFalse('virtual/passes/test-virtual-virtual/passes.html' in tests)
+        self.assertFalse('virtual/passes/virtual/passes/test-virtual-passes.html' in tests)
+
+    def test_build_path(self):
+        port = self.make_port(options=optparse.Values({'build_directory': '/my-build-directory/'}))
+        self.assertEqual(port._build_path(), '/my-build-directory/Release')
+
+    def test_dont_require_http_server(self):
+        port = self.make_port()
+        self.assertEqual(port.requires_http_server(), False)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/builders.py b/Tools/Scripts/webkitpy/layout_tests/port/builders.py
new file mode 100644
index 0000000..155ac89
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/builders.py
@@ -0,0 +1,154 @@
+#!/usr/bin/env python
+# Copyright (C) 2011 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 re
+
+from webkitpy.common.memoized import memoized
+
+
+# In this dictionary, each item stores:
+# * port_name -- a fully qualified port name
+# * specifiers -- a set of specifiers, representing configurations covered by this builder.
+# * move_overwritten_baselines_to -- (optional) list of platform directories that we will copy an existing
+#      baseline to before pulling down a new baseline during rebaselining. This is useful
+#      for bringing up a new port, for example when adding a Lion was the most recent Mac version and
+#      we wanted to bring up Mountain Lion, we would want to copy an existing baseline in platform/mac
+#      to platform/mac-mountainlion before updating the platform/mac entry.
+# * rebaseline_override_dir -- (optional) directory to put baselines in instead of where you would normally put them.
+#      This is useful when we don't have bots that cover particular configurations; so, e.g., you might
+#      support mac-mountainlion but not have a mac-mountainlion bot yet, so you'd want to put the mac-lion
+#      results into platform/mac temporarily.
+
+_exact_matches = {
+    # These builders are on build.chromium.org.
+    "WebKit XP": {"port_name": "chromium-win-xp", "specifiers": set(["xp", "release"])},
+    "WebKit Win7": {"port_name": "chromium-win-win7", "specifiers": set(["win7", "release"])},
+    "WebKit Win7 (dbg)(1)": {"port_name": "chromium-win-win7", "specifiers": set(["win7", "debug"])},
+    "WebKit Win7 (dbg)(2)": {"port_name": "chromium-win-win7", "specifiers": set(["win7", "debug"])},
+    "WebKit Linux": {"port_name": "chromium-linux-x86_64", "specifiers": set(["linux", "x86_64", "release"])},
+    "WebKit Linux 32": {"port_name": "chromium-linux-x86", "specifiers": set(["linux", "x86"])},
+    "WebKit Linux (dbg)": {"port_name": "chromium-linux-x86_64", "specifiers": set(["linux", "debug"])},
+    "WebKit Mac10.6": {"port_name": "chromium-mac-snowleopard", "specifiers": set(["snowleopard"])},
+    "WebKit Mac10.6 (dbg)": {"port_name": "chromium-mac-snowleopard", "specifiers": set(["snowleopard", "debug"])},
+    "WebKit Mac10.7": {"port_name": "chromium-mac-lion", "specifiers": set(["lion", "release"])},
+    "WebKit Mac10.7 (dbg)": {"port_name": "chromium-mac-lion", "specifiers": set(["lion", "debug"])},
+    "WebKit Mac10.8": {"port_name": "chromium-mac-mountainlion", "specifiers": set(["mountainlion", "release"]),
+                       "move_overwritten_baselines_to": ["chromium-mac-lion"]},
+
+    # These builders are on build.webkit.org.
+    "Apple MountainLion Release WK1 (Tests)": {"port_name": "mac-mountainlion", "specifiers": set(["mountainlion"]), "rebaseline_override_dir": "mac"},
+    "Apple MountainLion Debug WK1 (Tests)": {"port_name": "mac-mountainlion", "specifiers": set(["mountainlion", "debug"]), "rebaseline_override_dir": "mac"},
+    "Apple MountainLion Release WK2 (Tests)": {"port_name": "mac-mountainlion", "specifiers": set(["mountainlion", "wk2"]), "rebaseline_override_dir": "mac"},
+    "Apple MountainLion Debug WK2 (Tests)": {"port_name": "mac-mountainlion", "specifiers": set(["mountainlion", "wk2", "debug"]), "rebaseline_override_dir": "mac"},
+    "Apple Lion Release WK1 (Tests)": {"port_name": "mac-lion", "specifiers": set(["lion"])},
+    "Apple Lion Debug WK1 (Tests)": {"port_name": "mac-lion", "specifiers": set(["lion", "debug"])},
+    "Apple Lion Release WK2 (Tests)": {"port_name": "mac-lion", "specifiers": set(["lion", "wk2"])},
+    "Apple Lion Debug WK2 (Tests)": {"port_name": "mac-lion", "specifiers": set(["lion", "wk2", "debug"])},
+
+    "Apple Win XP Debug (Tests)": {"port_name": "win-xp", "specifiers": set(["win", "debug"])},
+    # FIXME: Remove rebaseline_override_dir once there is an Apple buildbot that corresponds to platform/win.
+    "Apple Win 7 Release (Tests)": {"port_name": "win-7sp0", "specifiers": set(["win"]), "rebaseline_override_dir": "win"},
+
+    "GTK Linux 32-bit Release": {"port_name": "gtk", "specifiers": set(["gtk", "x86", "release"])},
+    "GTK Linux 64-bit Debug": {"port_name": "gtk", "specifiers": set(["gtk", "x86_64", "debug"])},
+    "GTK Linux 64-bit Release": {"port_name": "gtk", "specifiers": set(["gtk", "x86_64", "release"])},
+    "GTK Linux 64-bit Release WK2 (Tests)": {"port_name": "gtk", "specifiers": set(["gtk", "x86_64", "wk2", "release"])},
+
+    # FIXME: Remove rebaseline_override_dir once there are Qt bots for all the platform/qt-* directories.
+    "Qt Linux Release": {"port_name": "qt-linux", "specifiers": set(["win", "linux", "mac"]), "rebaseline_override_dir": "qt"},
+
+    "EFL Linux 64-bit Debug": {"port_name": "efl", "specifiers": set(["efl", "debug"])},
+    "EFL Linux 64-bit Release": {"port_name": "efl", "specifiers": set(["efl", "release"])},
+}
+
+
+_fuzzy_matches = {
+    # These builders are on build.webkit.org.
+    r"SnowLeopard": "mac-snowleopard",
+    r"Apple Lion": "mac-lion",
+    r"Windows": "win",
+    r"GTK": "gtk",
+    r"Qt": "qt",
+    r"Chromium Mac": "chromium-mac",
+    r"Chromium Linux": "chromium-linux",
+    r"Chromium Win": "chromium-win",
+}
+
+
+_ports_without_builders = [
+    "qt-mac",
+    "qt-win",
+    "qt-wk2",
+    # FIXME: Move to _extact_matches.
+    "chromium-android",
+]
+
+
+def builder_path_from_name(builder_name):
+    return re.sub(r'[\s().]', '_', builder_name)
+
+
+def all_builder_names():
+    return sorted(set(_exact_matches.keys()))
+
+
+def all_port_names():
+    return sorted(set(map(lambda x: x["port_name"], _exact_matches.values()) + _ports_without_builders))
+
+
+def coverage_specifiers_for_builder_name(builder_name):
+    return _exact_matches[builder_name].get("specifiers", set())
+
+
+def rebaseline_override_dir(builder_name):
+    return _exact_matches[builder_name].get("rebaseline_override_dir", None)
+
+
+def move_overwritten_baselines_to(builder_name):
+    return _exact_matches[builder_name].get("move_overwritten_baselines_to", [])
+
+
+def port_name_for_builder_name(builder_name):
+    if builder_name in _exact_matches:
+        return _exact_matches[builder_name]["port_name"]
+
+    for regexp, port_name in _fuzzy_matches.items():
+        if re.match(regexp, builder_name):
+            return port_name
+
+
+def builder_name_for_port_name(target_port_name):
+    for builder_name, builder_info in _exact_matches.items():
+        if builder_info['port_name'] == target_port_name and 'debug' not in builder_info['specifiers']:
+            return builder_name
+    return None
+
+
+def builder_path_for_port_name(port_name):
+    builder_path_from_name(builder_name_for_port_name(port_name))
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/builders_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/builders_unittest.py
new file mode 100644
index 0000000..1550df4
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/builders_unittest.py
@@ -0,0 +1,44 @@
+# Copyright (C) 2011 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 builders
+import unittest
+
+
+class BuildersTest(unittest.TestCase):
+    def test_path_from_name(self):
+        tests = {
+            'test': 'test',
+            'Mac 10.6 (dbg)(1)': 'Mac_10_6__dbg__1_',
+            '(.) ': '____',
+        }
+        for name, expected in tests.items():
+            self.assertEquals(expected, builders.builder_path_from_name(name))
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium.py
new file mode 100755
index 0000000..a69f5a8
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium.py
@@ -0,0 +1,452 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Chromium implementations of the Port interface."""
+
+import base64
+import errno
+import logging
+import re
+import signal
+import subprocess
+import sys
+import time
+
+from webkitpy.common.system import executive
+from webkitpy.common.system.path import cygpath
+from webkitpy.layout_tests.models.test_configuration import TestConfiguration
+from webkitpy.layout_tests.port.base import Port, VirtualTestSuite
+
+
+_log = logging.getLogger(__name__)
+
+
+class ChromiumPort(Port):
+    """Abstract base class for Chromium implementations of the Port class."""
+
+    ALL_SYSTEMS = (
+        ('snowleopard', 'x86'),
+        ('lion', 'x86'),
+        ('mountainlion', 'x86'),
+        ('xp', 'x86'),
+        ('win7', 'x86'),
+        ('lucid', 'x86'),
+        ('lucid', 'x86_64'),
+        # FIXME: Technically this should be 'arm', but adding a third architecture type breaks TestConfigurationConverter.
+        # If we need this to be 'arm' in the future, then we first have to fix TestConfigurationConverter.
+        ('icecreamsandwich', 'x86'))
+
+    ALL_BASELINE_VARIANTS = [
+        'chromium-mac-mountainlion', 'chromium-mac-lion', 'chromium-mac-snowleopard',
+        'chromium-win-win7', 'chromium-win-xp',
+        'chromium-linux-x86_64', 'chromium-linux-x86',
+    ]
+
+    CONFIGURATION_SPECIFIER_MACROS = {
+        'mac': ['snowleopard', 'lion', 'mountainlion'],
+        'win': ['xp', 'win7'],
+        'linux': ['lucid'],
+        'android': ['icecreamsandwich'],
+    }
+
+    DEFAULT_BUILD_DIRECTORIES = ('out',)
+
+    @classmethod
+    def _static_build_path(cls, filesystem, build_directory, chromium_base, webkit_base, configuration, comps):
+        if build_directory:
+            return filesystem.join(build_directory, configuration, *comps)
+
+        for directory in cls.DEFAULT_BUILD_DIRECTORIES:
+            base_dir = filesystem.join(chromium_base, directory, configuration)
+            if filesystem.exists(base_dir):
+                return filesystem.join(base_dir, *comps)
+
+        for directory in cls.DEFAULT_BUILD_DIRECTORIES:
+            base_dir = filesystem.join(webkit_base, directory, configuration)
+            if filesystem.exists(base_dir):
+                return filesystem.join(base_dir, *comps)
+
+        # We have to default to something, so pick the last one.
+        return filesystem.join(base_dir, *comps)
+
+    @classmethod
+    def _chromium_base_dir(cls, filesystem):
+        module_path = filesystem.path_to_module(cls.__module__)
+        offset = module_path.find('third_party')
+        if offset == -1:
+            return filesystem.join(module_path[0:module_path.find('Tools')], 'Source', 'WebKit', 'chromium')
+        else:
+            return module_path[0:offset]
+
+    def __init__(self, host, port_name, **kwargs):
+        super(ChromiumPort, self).__init__(host, port_name, **kwargs)
+        # All sub-classes override this, but we need an initial value for testing.
+        self._chromium_base_dir_path = None
+
+    def is_chromium(self):
+        return True
+
+    def default_max_locked_shards(self):
+        """Return the number of "locked" shards to run in parallel (like the http tests)."""
+        max_locked_shards = int(self.default_child_processes()) / 4
+        if not max_locked_shards:
+            return 1
+        return max_locked_shards
+
+    def default_pixel_tests(self):
+        return True
+
+    def default_baseline_search_path(self):
+        return map(self._webkit_baseline_path, self.FALLBACK_PATHS[self.version()])
+
+    def default_timeout_ms(self):
+        if self.get_option('configuration') == 'Debug':
+            return 12 * 1000
+        return 6 * 1000
+
+    def _check_file_exists(self, path_to_file, file_description,
+                           override_step=None, logging=True):
+        """Verify the file is present where expected or log an error.
+
+        Args:
+            file_name: The (human friendly) name or description of the file
+                you're looking for (e.g., "HTTP Server"). Used for error logging.
+            override_step: An optional string to be logged if the check fails.
+            logging: Whether or not log the error messages."""
+        if not self._filesystem.exists(path_to_file):
+            if logging:
+                _log.error('Unable to find %s' % file_description)
+                _log.error('    at %s' % path_to_file)
+                if override_step:
+                    _log.error('    %s' % override_step)
+                    _log.error('')
+            return False
+        return True
+
+    def check_build(self, needs_http):
+        result = True
+
+        dump_render_tree_binary_path = self._path_to_driver()
+        result = self._check_file_exists(dump_render_tree_binary_path,
+                                         'test driver') and result
+        if result and self.get_option('build'):
+            result = self._check_driver_build_up_to_date(
+                self.get_option('configuration'))
+        else:
+            _log.error('')
+
+        helper_path = self._path_to_helper()
+        if helper_path:
+            result = self._check_file_exists(helper_path,
+                                             'layout test helper') and result
+
+        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 and wdiff aren't available, but we will at least log messages.
+        self._pretty_patch_available = self.check_pretty_patch()
+        self._wdiff_available = self.check_wdiff()
+
+        return result
+
+    def check_sys_deps(self, needs_http):
+        result = super(ChromiumPort, self).check_sys_deps(needs_http)
+
+        cmd = [self._path_to_driver(), '--check-layout-test-sys-deps']
+
+        local_error = executive.ScriptError()
+
+        def error_handler(script_error):
+            local_error.exit_code = script_error.exit_code
+
+        output = self._executive.run_command(cmd, error_handler=error_handler)
+        if local_error.exit_code:
+            _log.error('System dependencies check failed.')
+            _log.error('To override, invoke with --nocheck-sys-deps')
+            _log.error('')
+            _log.error(output)
+            return False
+        return result
+
+    def check_image_diff(self, override_step=None, logging=True):
+        image_diff_path = self._path_to_image_diff()
+        return self._check_file_exists(image_diff_path, 'image diff exe',
+                                       override_step, logging)
+
+    def diff_image(self, expected_contents, actual_contents, tolerance=None):
+        # tolerance is not used in chromium. Make sure caller doesn't pass tolerance other than zero or None.
+        assert (tolerance is None) or tolerance == 0
+
+        # If only one of them exists, return that one.
+        if not actual_contents and not expected_contents:
+            return (None, 0, None)
+        if not actual_contents:
+            return (expected_contents, 0, None)
+        if not expected_contents:
+            return (actual_contents, 0, None)
+
+        tempdir = self._filesystem.mkdtemp()
+
+        expected_filename = self._filesystem.join(str(tempdir), "expected.png")
+        self._filesystem.write_binary_file(expected_filename, expected_contents)
+
+        actual_filename = self._filesystem.join(str(tempdir), "actual.png")
+        self._filesystem.write_binary_file(actual_filename, actual_contents)
+
+        diff_filename = self._filesystem.join(str(tempdir), "diff.png")
+
+        native_expected_filename = self._convert_path(expected_filename)
+        native_actual_filename = self._convert_path(actual_filename)
+        native_diff_filename = self._convert_path(diff_filename)
+
+        executable = self._path_to_image_diff()
+        # Note that although we are handed 'old', 'new', image_diff wants 'new', 'old'.
+        comand = [executable, '--diff', native_actual_filename, native_expected_filename, native_diff_filename]
+
+        result = None
+        err_str = None
+        try:
+            exit_code = self._executive.run_command(comand, return_exit_code=True)
+            if exit_code == 0:
+                # The images are the same.
+                result = None
+            elif exit_code == 1:
+                result = self._filesystem.read_binary_file(native_diff_filename)
+            else:
+                err_str = "image diff returned an exit code of %s" % exit_code
+        except OSError, e:
+            err_str = 'error running image diff: %s' % str(e)
+        finally:
+            self._filesystem.rmtree(str(tempdir))
+
+        return (result, 0, err_str or None)  # FIXME: how to get % diff?
+
+    def path_from_chromium_base(self, *comps):
+        """Returns the full path to path made by joining the top of the
+        Chromium source tree and the list of path components in |*comps|."""
+        if self._chromium_base_dir_path is None:
+            self._chromium_base_dir_path = self._chromium_base_dir(self._filesystem)
+        return self._filesystem.join(self._chromium_base_dir_path, *comps)
+
+    def setup_environ_for_server(self, server_name=None):
+        clean_env = super(ChromiumPort, self).setup_environ_for_server(server_name)
+        # Webkit Linux (valgrind layout) bot needs these envvars.
+        self._copy_value_from_environ_if_set(clean_env, 'VALGRIND_LIB')
+        self._copy_value_from_environ_if_set(clean_env, 'VALGRIND_LIB_INNER')
+        return clean_env
+
+    def default_results_directory(self):
+        try:
+            return self.path_from_chromium_base('webkit', self.get_option('configuration'), 'layout-test-results')
+        except AssertionError:
+            return self._build_path('layout-test-results')
+
+    def _missing_symbol_to_skipped_tests(self):
+        # FIXME: Should WebKitPort have these definitions also?
+        return {
+            "ff_mp3_decoder": ["webaudio/codec-tests/mp3"],
+            "ff_aac_decoder": ["webaudio/codec-tests/aac"],
+        }
+
+    def skipped_layout_tests(self, test_list):
+        # FIXME: Merge w/ WebKitPort.skipped_layout_tests()
+        return set(self._skipped_tests_for_unsupported_features(test_list))
+
+    def setup_test_run(self):
+        # Delete the disk cache if any to ensure a clean test run.
+        dump_render_tree_binary_path = self._path_to_driver()
+        cachedir = self._filesystem.dirname(dump_render_tree_binary_path)
+        cachedir = self._filesystem.join(cachedir, "cache")
+        if self._filesystem.exists(cachedir):
+            self._filesystem.rmtree(cachedir)
+
+    def start_helper(self):
+        helper_path = self._path_to_helper()
+        if helper_path:
+            _log.debug("Starting layout helper %s" % helper_path)
+            # Note: Not thread safe: http://bugs.python.org/issue2320
+            self._helper = subprocess.Popen([helper_path],
+                stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None)
+            is_ready = self._helper.stdout.readline()
+            if not is_ready.startswith('ready'):
+                _log.error("layout_test_helper failed to be ready")
+
+    def stop_helper(self):
+        if self._helper:
+            _log.debug("Stopping layout test helper")
+            try:
+                self._helper.stdin.write("x\n")
+                self._helper.stdin.close()
+                self._helper.wait()
+            except IOError, e:
+                pass
+            finally:
+                self._helper = None
+
+
+    def exit_code_from_summarized_results(self, unexpected_results):
+        # Turn bots red for missing results.
+        return unexpected_results['num_regressions'] + unexpected_results['num_missing']
+
+    def configuration_specifier_macros(self):
+        return self.CONFIGURATION_SPECIFIER_MACROS
+
+    def all_baseline_variants(self):
+        return self.ALL_BASELINE_VARIANTS
+
+    def _generate_all_test_configurations(self):
+        """Returns a sequence of the TestConfigurations the port supports."""
+        # By default, we assume we want to test every graphics type in
+        # every configuration on every system.
+        test_configurations = []
+        for version, architecture in self.ALL_SYSTEMS:
+            for build_type in self.ALL_BUILD_TYPES:
+                test_configurations.append(TestConfiguration(version, architecture, build_type))
+        return test_configurations
+
+    try_builder_names = frozenset([
+        'linux_layout',
+        'mac_layout',
+        'win_layout',
+        'linux_layout_rel',
+        'mac_layout_rel',
+        'win_layout_rel',
+    ])
+
+    def warn_if_bug_missing_in_test_expectations(self):
+        return True
+
+    def expectations_files(self):
+        paths = [self.path_to_test_expectations_file()]
+        skia_expectations_path = self.path_from_chromium_base('skia', 'skia_test_expectations.txt')
+        # FIXME: we should probably warn if this file is missing in some situations.
+        # See the discussion in webkit.org/b/97699.
+        if self._filesystem.exists(skia_expectations_path):
+            paths.append(skia_expectations_path)
+
+        builder_name = self.get_option('builder_name', 'DUMMY_BUILDER_NAME')
+        if builder_name == 'DUMMY_BUILDER_NAME' or '(deps)' in builder_name or builder_name in self.try_builder_names:
+            paths.append(self.path_from_chromium_base('webkit', 'tools', 'layout_tests', 'test_expectations.txt'))
+        return paths
+
+    def repository_paths(self):
+        repos = super(ChromiumPort, self).repository_paths()
+        repos.append(('chromium', self.path_from_chromium_base('build')))
+        return repos
+
+    def _get_crash_log(self, name, pid, stdout, stderr, newer_than):
+        if stderr and 'AddressSanitizer' in stderr:
+            asan_filter_path = self.path_from_chromium_base('tools', 'valgrind', 'asan', 'asan_symbolize.py')
+            if self._filesystem.exists(asan_filter_path):
+                output = self._executive.run_command([asan_filter_path], input=stderr, decode_output=False)
+                stderr = self._executive.run_command(['c++filt'], input=output, decode_output=False)
+
+        return super(ChromiumPort, self)._get_crash_log(name, pid, stdout, stderr, newer_than)
+
+    def virtual_test_suites(self):
+        return [
+            VirtualTestSuite('platform/chromium/virtual/gpu/fast/canvas',
+                             'fast/canvas',
+                             ['--enable-accelerated-2d-canvas']),
+            VirtualTestSuite('platform/chromium/virtual/gpu/canvas/philip',
+                             'canvas/philip',
+                             ['--enable-accelerated-2d-canvas']),
+            VirtualTestSuite('platform/chromium/virtual/threaded/compositing/visibility',
+                             'compositing/visibility',
+                             ['--enable-threaded-compositing']),
+            VirtualTestSuite('platform/chromium/virtual/threaded/compositing/webgl',
+                             'compositing/webgl',
+                             ['--enable-threaded-compositing']),
+            VirtualTestSuite('platform/chromium/virtual/gpu/fast/hidpi',
+                             'fast/hidpi',
+                             ['--force-compositing-mode']),
+            VirtualTestSuite('platform/chromium/virtual/softwarecompositing',
+                             'compositing',
+                             ['--enable-software-compositing']),
+            VirtualTestSuite('platform/chromium/virtual/deferred/fast/images',
+                             'fast/images',
+                             ['--enable-deferred-image-decoding', '--enable-per-tile-painting', '--force-compositing-mode']),
+        ]
+
+    #
+    # PROTECTED METHODS
+    #
+    # These routines should only be called by other methods in this file
+    # or any subclasses.
+    #
+
+    def _build_path(self, *comps):
+        return self._build_path_with_configuration(None, *comps)
+
+    def _build_path_with_configuration(self, configuration, *comps):
+        # Note that we don't implement --root or do the option caching that the
+        # base class does, because chromium doesn't use 'webkit-build-directory' and
+        # hence finding the right directory is relatively fast.
+        configuration = configuration or self.get_option('configuration')
+        return self._static_build_path(self._filesystem, self.get_option('build_directory'),
+            self.path_from_chromium_base(), self.path_from_webkit_base(), configuration, comps)
+
+    def _path_to_image_diff(self):
+        binary_name = 'ImageDiff'
+        return self._build_path(binary_name)
+
+    def _check_driver_build_up_to_date(self, configuration):
+        if configuration in ('Debug', 'Release'):
+            try:
+                debug_path = self._path_to_driver('Debug')
+                release_path = self._path_to_driver('Release')
+
+                debug_mtime = self._filesystem.mtime(debug_path)
+                release_mtime = self._filesystem.mtime(release_path)
+
+                if (debug_mtime > release_mtime and configuration == 'Release' or
+                    release_mtime > debug_mtime and configuration == 'Debug'):
+                    most_recent_binary = 'Release' if configuration == 'Debug' else 'Debug'
+                    _log.warning('You are running the %s binary. However the %s binary appears to be more recent. '
+                                 'Please pass --%s.', configuration, most_recent_binary, most_recent_binary.lower())
+                    _log.warning('')
+            # This will fail if we don't have both a debug and release binary.
+            # That's fine because, in this case, we must already be running the
+            # most up-to-date one.
+            except OSError:
+                pass
+        return True
+
+    def _chromium_baseline_path(self, platform):
+        if platform is None:
+            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
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_android.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_android.py
new file mode 100644
index 0000000..b8ac55a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_android.py
@@ -0,0 +1,675 @@
+#!/usr/bin/env python
+# Copyright (C) 2012 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 copy
+import logging
+import os
+import re
+import subprocess
+import threading
+import time
+
+from webkitpy.layout_tests.port import chromium
+from webkitpy.layout_tests.port import driver
+from webkitpy.layout_tests.port import factory
+from webkitpy.layout_tests.port import server_process
+
+
+_log = logging.getLogger(__name__)
+
+
+# The root directory for test resources, which has the same structure as the
+# source root directory of Chromium.
+# This path is defined in Chromium's base/test/test_support_android.cc.
+DEVICE_SOURCE_ROOT_DIR = '/data/local/tmp/'
+COMMAND_LINE_FILE = DEVICE_SOURCE_ROOT_DIR + 'chrome-native-tests-command-line'
+
+# The directory to put tools and resources of DumpRenderTree.
+# If change this, must also change Tools/DumpRenderTree/chromium/TestShellAndroid.cpp
+# and Chromium's webkit/support/platform_support_android.cc.
+DEVICE_DRT_DIR = DEVICE_SOURCE_ROOT_DIR + 'drt/'
+DEVICE_FORWARDER_PATH = DEVICE_DRT_DIR + 'forwarder'
+
+# Path on the device where the test framework will create the fifo pipes.
+DEVICE_FIFO_PATH = '/data/data/org.chromium.native_test/files/'
+
+DRT_APP_PACKAGE = 'org.chromium.native_test'
+DRT_ACTIVITY_FULL_NAME = DRT_APP_PACKAGE + '/.ChromeNativeTestActivity'
+DRT_APP_CACHE_DIR = DEVICE_DRT_DIR + 'cache/'
+
+SCALING_GOVERNORS_PATTERN = "/sys/devices/system/cpu/cpu*/cpufreq/scaling_governor"
+
+# All the test cases are still served to DumpRenderTree through file protocol,
+# but we use a file-to-http feature to bridge the file request to host's http
+# server to get the real test files and corresponding resources.
+TEST_PATH_PREFIX = '/all-tests'
+
+# All ports the Android forwarder to forward.
+# 8000, 8080 and 8443 are for http/https tests.
+# 8880 and 9323 are for websocket tests
+# (see http_server.py, apache_http_server.py and websocket_server.py).
+FORWARD_PORTS = '8000 8080 8443 8880 9323'
+
+MS_TRUETYPE_FONTS_DIR = '/usr/share/fonts/truetype/msttcorefonts/'
+MS_TRUETYPE_FONTS_PACKAGE = 'ttf-mscorefonts-installer'
+
+# Timeout in seconds to wait for start/stop of DumpRenderTree.
+DRT_START_STOP_TIMEOUT_SECS = 10
+
+# List of fonts that layout tests expect, copied from DumpRenderTree/chromium/TestShellX11.cpp.
+HOST_FONT_FILES = [
+    [[MS_TRUETYPE_FONTS_DIR], 'Arial.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Arial_Bold.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Arial_Bold_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Arial_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Comic_Sans_MS.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Comic_Sans_MS_Bold.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Courier_New.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Courier_New_Bold.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Courier_New_Bold_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Courier_New_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Georgia.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Georgia_Bold.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Georgia_Bold_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Georgia_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Impact.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Trebuchet_MS.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Trebuchet_MS_Bold.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Trebuchet_MS_Bold_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Trebuchet_MS_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Times_New_Roman.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Times_New_Roman_Bold.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Times_New_Roman_Bold_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Times_New_Roman_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Verdana.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Verdana_Bold.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Verdana_Bold_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    [[MS_TRUETYPE_FONTS_DIR], 'Verdana_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE],
+    # The Microsoft font EULA
+    [['/usr/share/doc/ttf-mscorefonts-installer/'], 'READ_ME!.gz', MS_TRUETYPE_FONTS_PACKAGE],
+    # Other fonts: Arabic, CJK, Indic, Thai, etc.
+    [['/usr/share/fonts/truetype/ttf-dejavu/'], 'DejaVuSans.ttf', 'ttf-dejavu'],
+    [['/usr/share/fonts/truetype/kochi/'], 'kochi-mincho.ttf', 'ttf-kochi-mincho'],
+    [['/usr/share/fonts/truetype/ttf-indic-fonts-core/'], 'lohit_hi.ttf', 'ttf-indic-fonts-core'],
+    [['/usr/share/fonts/truetype/ttf-indic-fonts-core/'], 'lohit_ta.ttf', 'ttf-indic-fonts-core'],
+    [['/usr/share/fonts/truetype/ttf-indic-fonts-core/'], 'MuktiNarrow.ttf', 'ttf-indic-fonts-core'],
+    [['/usr/share/fonts/truetype/thai/', '/usr/share/fonts/truetype/tlwg/'], 'Garuda.ttf', 'fonts-tlwg-garuda'],
+    [['/usr/share/fonts/truetype/ttf-indic-fonts-core/', '/usr/share/fonts/truetype/ttf-punjabi-fonts/'], 'lohit_pa.ttf', 'ttf-indic-fonts-core'],
+]
+
+DEVICE_FONTS_DIR = DEVICE_DRT_DIR + 'fonts/'
+
+# The layout tests directory on device, which has two usages:
+# 1. as a virtual path in file urls that will be bridged to HTTP.
+# 2. pointing to some files that are pushed to the device for tests that
+# don't work on file-over-http (e.g. blob protocol tests).
+DEVICE_LAYOUT_TESTS_DIR = DEVICE_SOURCE_ROOT_DIR + 'third_party/WebKit/LayoutTests/'
+
+# Test resources that need to be accessed as files directly.
+# Each item can be the relative path of a directory or a file.
+TEST_RESOURCES_TO_PUSH = [
+    # Blob tests need to access files directly.
+    'editing/pasteboard/resources',
+    'fast/files/resources',
+    'http/tests/local/resources',
+    'http/tests/local/formdata/resources',
+    # User style URLs are accessed as local files in webkit_support.
+    'http/tests/security/resources/cssStyle.css',
+    # Media tests need to access audio/video as files.
+    'media/content',
+    'compositing/resources/video.mp4',
+]
+
+MD5SUM_DEVICE_FILE_NAME = 'md5sum_bin'
+MD5SUM_DEVICE_PATH = '/data/local/tmp/' + MD5SUM_DEVICE_FILE_NAME
+
+class ChromiumAndroidPort(chromium.ChromiumPort):
+    port_name = 'chromium-android'
+
+    FALLBACK_PATHS = [
+        'chromium-android',
+        'chromium-linux',
+        'chromium-win',
+        'chromium',
+    ]
+
+    def __init__(self, host, port_name, **kwargs):
+        super(ChromiumAndroidPort, self).__init__(host, port_name, **kwargs)
+
+        self._operating_system = 'android'
+        self._version = 'icecreamsandwich'
+
+        self._host_port = factory.PortFactory(host).get('chromium', **kwargs)
+        self._server_process_constructor = self._android_server_process_constructor
+
+        if hasattr(self._options, 'adb_device'):
+            self._devices = self._options.adb_device
+        else:
+            self._devices = []
+
+    @staticmethod
+    def _android_server_process_constructor(port, server_name, cmd_line, env=None):
+        return server_process.ServerProcess(port, server_name, cmd_line, env,
+                                            universal_newlines=True, treat_no_data_as_crash=True)
+
+    def additional_drt_flag(self):
+        # The Chromium port for Android always uses the hardware GPU path.
+        return ['--encode-binary', '--enable-hardware-gpu',
+                '--force-compositing-mode',
+                '--enable-accelerated-fixed-position']
+
+    def default_timeout_ms(self):
+        # Android platform has less computing power than desktop platforms.
+        # Using 10 seconds allows us to pass most slow tests which are not
+        # marked as slow tests on desktop platforms.
+        return 10 * 1000
+
+    def driver_stop_timeout(self):
+        # DRT doesn't respond to closing stdin, so we might as well stop the driver immediately.
+        return 0.0
+
+    def default_child_processes(self):
+        return len(self._get_devices())
+
+    def default_baseline_search_path(self):
+        return map(self._webkit_baseline_path, self.FALLBACK_PATHS)
+
+    def check_wdiff(self, logging=True):
+        return self._host_port.check_wdiff(logging)
+
+    def check_build(self, needs_http):
+        result = super(ChromiumAndroidPort, self).check_build(needs_http)
+        result = self._check_file_exists(self._path_to_md5sum(), 'md5sum utility') and result
+        result = self._check_file_exists(self._path_to_forwarder(), 'forwarder utility') and result
+        if not result:
+            _log.error('For complete Android build requirements, please see:')
+            _log.error('')
+            _log.error('    http://code.google.com/p/chromium/wiki/AndroidBuildInstructions')
+
+        return result
+
+    def check_sys_deps(self, needs_http):
+        for (font_dirs, font_file, package) in HOST_FONT_FILES:
+            exists = False
+            for font_dir in font_dirs:
+                font_path = font_dir + font_file
+                if self._check_file_exists(font_path, '', logging=False):
+                    exists = True
+                    break
+            if not exists:
+                _log.error('You are missing %s under %s. Try installing %s. See build instructions.' % (font_file, font_dirs, package))
+                return False
+        return True
+
+    def expectations_files(self):
+        # LayoutTests/platform/chromium-android/TestExpectations should contain only the rules to
+        # skip tests for the features not supported or not testable on Android.
+        # Other rules should be in LayoutTests/platform/chromium/TestExpectations.
+        android_expectations_file = self.path_from_webkit_base('LayoutTests', 'platform', 'chromium-android', 'TestExpectations')
+        return super(ChromiumAndroidPort, self).expectations_files() + [android_expectations_file]
+
+    def requires_http_server(self):
+        """Chromium Android runs tests on devices, and uses the HTTP server to
+        serve the actual layout tests to DumpRenderTree."""
+        return True
+
+    def start_http_server(self, additional_dirs=None, number_of_servers=0):
+        if not additional_dirs:
+            additional_dirs = {}
+        additional_dirs[TEST_PATH_PREFIX] = self.layout_tests_dir()
+        super(ChromiumAndroidPort, self).start_http_server(additional_dirs, number_of_servers)
+
+    def create_driver(self, worker_number, no_timeout=False):
+        # We don't want the default DriverProxy which is not compatible with our driver.
+        # See comments in ChromiumAndroidDriver.start().
+        return ChromiumAndroidDriver(self, worker_number, pixel_tests=self.get_option('pixel_tests'),
+                                     # Force no timeout to avoid DumpRenderTree timeouts before NRWT.
+                                     no_timeout=True)
+
+    def driver_cmd_line(self):
+        # Override to return the actual DumpRenderTree command line.
+        return self.create_driver(0)._drt_cmd_line(self.get_option('pixel_tests'), [])
+
+    # Overridden private functions.
+
+    def _build_path(self, *comps):
+        return self._host_port._build_path(*comps)
+
+    def _build_path_with_configuration(self, configuration, *comps):
+        return self._host_port._build_path_with_configuration(configuration, *comps)
+
+    def _path_to_apache(self):
+        return self._host_port._path_to_apache()
+
+    def _path_to_apache_config_file(self):
+        return self._host_port._path_to_apache_config_file()
+
+    def _path_to_driver(self, configuration=None):
+        return self._build_path_with_configuration(configuration, 'DumpRenderTree_apk/DumpRenderTree-debug.apk')
+
+    def _path_to_helper(self):
+        return None
+
+    def _path_to_forwarder(self):
+        return self._build_path('forwarder')
+
+    def _path_to_md5sum(self):
+        return self._build_path(MD5SUM_DEVICE_FILE_NAME)
+
+    def _path_to_image_diff(self):
+        return self._host_port._path_to_image_diff()
+
+    def _path_to_lighttpd(self):
+        return self._host_port._path_to_lighttpd()
+
+    def _path_to_lighttpd_modules(self):
+        return self._host_port._path_to_lighttpd_modules()
+
+    def _path_to_lighttpd_php(self):
+        return self._host_port._path_to_lighttpd_php()
+
+    def _path_to_wdiff(self):
+        return self._host_port._path_to_wdiff()
+
+    def _shut_down_http_server(self, pid):
+        return self._host_port._shut_down_http_server(pid)
+
+    def _driver_class(self):
+        return ChromiumAndroidDriver
+
+    # Local private functions.
+
+    def _get_devices(self):
+        if not self._devices:
+            re_device = re.compile('^([a-zA-Z0-9_:.-]+)\tdevice$', re.MULTILINE)
+            result = self._executive.run_command(['adb', 'devices'], error_handler=self._executive.ignore_error)
+            self._devices = re_device.findall(result)
+            if not self._devices:
+                raise AssertionError('No devices attached. Result of "adb devices": %s' % result)
+        return self._devices
+
+    def _get_device_serial(self, worker_number):
+        devices = self._get_devices()
+        if worker_number >= len(devices):
+            raise AssertionError('Worker number exceeds available number of devices')
+        return devices[worker_number]
+
+
+class ChromiumAndroidDriver(driver.Driver):
+    def __init__(self, port, worker_number, pixel_tests, no_timeout=False):
+        super(ChromiumAndroidDriver, self).__init__(port, worker_number, pixel_tests, no_timeout)
+        self._cmd_line = None
+        self._in_fifo_path = DEVICE_FIFO_PATH + 'stdin.fifo'
+        self._out_fifo_path = DEVICE_FIFO_PATH + 'test.fifo'
+        self._err_fifo_path = DEVICE_FIFO_PATH + 'stderr.fifo'
+        self._read_stdout_process = None
+        self._read_stderr_process = None
+        self._forwarder_process = None
+        self._has_setup = False
+        self._original_governors = {}
+        self._device_serial = port._get_device_serial(worker_number)
+        self._adb_command = ['adb', '-s', self._device_serial]
+
+    def __del__(self):
+        self._teardown_performance()
+        super(ChromiumAndroidDriver, self).__del__()
+
+    def _setup_md5sum_and_push_data_if_needed(self):
+        self._md5sum_path = self._port._path_to_md5sum()
+        if not self._file_exists_on_device(MD5SUM_DEVICE_PATH):
+            if not self._push_to_device(self._md5sum_path, MD5SUM_DEVICE_PATH):
+                raise AssertionError('Could not push md5sum to device')
+
+        self._push_executable()
+        self._push_fonts()
+        self._push_test_resources()
+
+    def _setup_test(self):
+        if self._has_setup:
+            return
+
+        self._setup_md5sum_and_push_data_if_needed()
+        self._has_setup = True
+        self._run_adb_command(['root'])
+        self._setup_performance()
+        # Required by webkit_support::GetWebKitRootDirFilePath().
+        # Other directories will be created automatically by adb push.
+        self._run_adb_command(['shell', 'mkdir', '-p', DEVICE_SOURCE_ROOT_DIR + 'chrome'])
+
+        # Allow the DumpRenderTree app to fully access the directory.
+        # The native code needs the permission to write temporary files and create pipes here.
+        self._run_adb_command(['shell', 'mkdir', '-p', DEVICE_DRT_DIR])
+        self._run_adb_command(['shell', 'chmod', '777', DEVICE_DRT_DIR])
+
+        # Delete the disk cache if any to ensure a clean test run.
+        # This is like what's done in ChromiumPort.setup_test_run but on the device.
+        self._run_adb_command(['shell', 'rm', '-r', DRT_APP_CACHE_DIR])
+
+    def _log_error(self, message):
+        _log.error('[%s] %s' % (self._device_serial, message))
+
+    def _log_debug(self, message):
+        _log.debug('[%s] %s' % (self._device_serial, message))
+
+    def _abort(self, message):
+        raise AssertionError('[%s] %s' % (self._device_serial, message))
+
+    @staticmethod
+    def _extract_hashes_from_md5sum_output(md5sum_output):
+        assert md5sum_output
+        return [line.split('  ')[0] for line in md5sum_output]
+
+    def _push_file_if_needed(self, host_file, device_file):
+        assert os.path.exists(host_file)
+        device_hashes = self._extract_hashes_from_md5sum_output(
+                self._port.host.executive.popen(self._adb_command + ['shell', MD5SUM_DEVICE_PATH, device_file],
+                                                stdout=subprocess.PIPE).stdout)
+        host_hashes = self._extract_hashes_from_md5sum_output(
+                self._port.host.executive.popen(args=['%s_host' % self._md5sum_path, host_file],
+                                                stdout=subprocess.PIPE).stdout)
+        if host_hashes and device_hashes == host_hashes:
+            return
+        self._push_to_device(host_file, device_file)
+
+    def _push_executable(self):
+        self._push_file_if_needed(self._port._path_to_forwarder(), DEVICE_FORWARDER_PATH)
+        self._push_file_if_needed(self._port._build_path('DumpRenderTree.pak'), DEVICE_DRT_DIR + 'DumpRenderTree.pak')
+        self._push_file_if_needed(self._port._build_path('DumpRenderTree_resources'), DEVICE_DRT_DIR + 'DumpRenderTree_resources')
+        self._push_file_if_needed(self._port._build_path('android_main_fonts.xml'), DEVICE_DRT_DIR + 'android_main_fonts.xml')
+        self._push_file_if_needed(self._port._build_path('android_fallback_fonts.xml'), DEVICE_DRT_DIR + 'android_fallback_fonts.xml')
+        self._run_adb_command(['uninstall', DRT_APP_PACKAGE])
+        drt_host_path = self._port._path_to_driver()
+        install_result = self._run_adb_command(['install', drt_host_path])
+        if install_result.find('Success') == -1:
+            self._abort('Failed to install %s onto device: %s' % (drt_host_path, install_result))
+
+    def _push_fonts(self):
+        self._log_debug('Pushing fonts')
+        path_to_ahem_font = self._port._build_path('AHEM____.TTF')
+        self._push_file_if_needed(path_to_ahem_font, DEVICE_FONTS_DIR + 'AHEM____.TTF')
+        for (host_dirs, font_file, package) in HOST_FONT_FILES:
+            for host_dir in host_dirs:
+                host_font_path = host_dir + font_file
+                if self._port._check_file_exists(host_font_path, '', logging=False):
+                    self._push_file_if_needed(host_font_path, DEVICE_FONTS_DIR + font_file)
+
+    def _push_test_resources(self):
+        self._log_debug('Pushing test resources')
+        for resource in TEST_RESOURCES_TO_PUSH:
+            self._push_file_if_needed(self._port.layout_tests_dir() + '/' + resource, DEVICE_LAYOUT_TESTS_DIR + resource)
+
+    def _run_adb_command(self, cmd, ignore_error=False):
+        self._log_debug('Run adb command: ' + str(cmd))
+        if ignore_error:
+            error_handler = self._port._executive.ignore_error
+        else:
+            error_handler = None
+        result = self._port._executive.run_command(self._adb_command + cmd, error_handler=error_handler)
+        # Limit the length to avoid too verbose output of commands like 'adb logcat' and 'cat /data/tombstones/tombstone01'
+        # whose outputs are normally printed in later logs.
+        self._log_debug('Run adb result: ' + result[:80])
+        return result
+
+    def _link_device_file(self, from_file, to_file, ignore_error=False):
+        # rm to_file first to make sure that ln succeeds.
+        self._run_adb_command(['shell', 'rm', to_file], ignore_error)
+        return self._run_adb_command(['shell', 'ln', '-s', from_file, to_file], ignore_error)
+
+    def _push_to_device(self, host_path, device_path, ignore_error=False):
+        return self._run_adb_command(['push', host_path, device_path], ignore_error)
+
+    def _pull_from_device(self, device_path, host_path, ignore_error=False):
+        return self._run_adb_command(['pull', device_path, host_path], ignore_error)
+
+    def _get_last_stacktrace(self):
+        tombstones = self._run_adb_command(['shell', 'ls', '-n', '/data/tombstones'])
+        if not tombstones or tombstones.startswith('/data/tombstones: No such file or directory'):
+            self._log_error('DRT crashed, but no tombstone found!')
+            return ''
+        tombstones = tombstones.rstrip().split('\n')
+        last_tombstone = tombstones[0].split()
+        for tombstone in tombstones[1:]:
+            # Format of fields:
+            # 0          1      2      3     4          5     6
+            # permission uid    gid    size  date       time  filename
+            # -rw------- 1000   1000   45859 2011-04-13 06:00 tombstone_00
+            fields = tombstone.split()
+            if (fields[4] + fields[5] >= last_tombstone[4] + last_tombstone[5]):
+                last_tombstone = fields
+            else:
+                break
+
+        # Use Android tool vendor/google/tools/stack to convert the raw
+        # stack trace into a human readable format, if needed.
+        # It takes a long time, so don't do it here.
+        return '%s\n%s' % (' '.join(last_tombstone),
+                           self._run_adb_command(['shell', 'cat', '/data/tombstones/' + last_tombstone[6]]))
+
+    def _get_logcat(self):
+        return self._run_adb_command(['logcat', '-d', '-v', 'threadtime'])
+
+    def _setup_performance(self):
+        # Disable CPU scaling and drop ram cache to reduce noise in tests
+        if not self._original_governors:
+            governor_files = self._run_adb_command(['shell', 'ls', SCALING_GOVERNORS_PATTERN])
+            if governor_files.find('No such file or directory') == -1:
+                for file in governor_files.split():
+                    self._original_governors[file] = self._run_adb_command(['shell', 'cat', file]).strip()
+                    self._run_adb_command(['shell', 'echo', 'performance', '>', file])
+
+    def _teardown_performance(self):
+        for file, original_content in self._original_governors.items():
+            self._run_adb_command(['shell', 'echo', original_content, '>', file])
+        self._original_governors = {}
+
+    def _command_wrapper(cls, wrapper_option):
+        # Ignore command wrapper which is not applicable on Android.
+        return []
+
+    def _get_crash_log(self, stdout, stderr, newer_than):
+        if not stdout:
+            stdout = ''
+        stdout += '********* [%s] Logcat:\n%s' % (self._device_serial, self._get_logcat())
+        if not stderr:
+            stderr = ''
+        stderr += '********* [%s] Tombstone file:\n%s' % (self._device_serial, self._get_last_stacktrace())
+        return super(ChromiumAndroidDriver, self)._get_crash_log(stdout, stderr, newer_than)
+
+    def cmd_line(self, pixel_tests, per_test_args):
+        # The returned command line is used to start _server_process. In our case, it's an interactive 'adb shell'.
+        # The command line passed to the DRT process is returned by _drt_cmd_line() instead.
+        return self._adb_command + ['shell']
+
+    def _file_exists_on_device(self, full_file_path):
+        assert full_file_path.startswith('/')
+        return self._run_adb_command(['shell', 'ls', full_file_path]).strip() == full_file_path
+
+    def _drt_cmd_line(self, pixel_tests, per_test_args):
+        return driver.Driver.cmd_line(self, pixel_tests, per_test_args) + ['--create-stdin-fifo', '--separate-stderr-fifo']
+
+    @staticmethod
+    def _loop_with_timeout(condition, timeout_secs):
+        deadline = time.time() + timeout_secs
+        while time.time() < deadline:
+            if condition():
+                return True
+        return False
+
+    def _all_pipes_created(self):
+        return (self._file_exists_on_device(self._in_fifo_path) and
+                self._file_exists_on_device(self._out_fifo_path) and
+                self._file_exists_on_device(self._err_fifo_path))
+
+    def _remove_all_pipes(self):
+        for file in [self._in_fifo_path, self._out_fifo_path, self._err_fifo_path]:
+            self._run_adb_command(['shell', 'rm', file])
+
+        return (not self._file_exists_on_device(self._in_fifo_path) and
+                not self._file_exists_on_device(self._out_fifo_path) and
+                not self._file_exists_on_device(self._err_fifo_path))
+
+    def run_test(self, driver_input, stop_when_done):
+        base = self._port.lookup_virtual_test_base(driver_input.test_name)
+        if base:
+            driver_input = copy.copy(driver_input)
+            driver_input.args = self._port.lookup_virtual_test_args(driver_input.test_name)
+            driver_input.test_name = base
+        return super(ChromiumAndroidDriver, self).run_test(driver_input, stop_when_done)
+
+    def start(self, pixel_tests, per_test_args):
+        # Only one driver instance is allowed because of the nature of Android activity.
+        # The single driver needs to restart DumpRenderTree when the command line changes.
+        cmd_line = self._drt_cmd_line(pixel_tests, per_test_args)
+        if cmd_line != self._cmd_line:
+            self.stop()
+            self._cmd_line = cmd_line
+        super(ChromiumAndroidDriver, self).start(pixel_tests, per_test_args)
+
+    def _start(self, pixel_tests, per_test_args):
+        self._setup_test()
+
+        for retries in range(3):
+            if self._start_once(pixel_tests, per_test_args):
+                return
+            self._log_error('Failed to start DumpRenderTree application. Retries=%d. Log:%s' % (retries, self._get_logcat()))
+            self.stop()
+            time.sleep(2)
+        self._abort('Failed to start DumpRenderTree application multiple times. Give up.')
+
+    def _start_once(self, pixel_tests, per_test_args):
+        super(ChromiumAndroidDriver, self)._start(pixel_tests, per_test_args)
+
+        self._log_debug('Starting forwarder')
+        self._forwarder_process = self._port._server_process_constructor(
+            self._port, 'Forwarder', self._adb_command + ['shell', '%s -D %s' % (DEVICE_FORWARDER_PATH, FORWARD_PORTS)])
+        self._forwarder_process.start()
+
+        self._run_adb_command(['logcat', '-c'])
+        self._run_adb_command(['shell', 'echo'] + self._cmd_line + ['>', COMMAND_LINE_FILE])
+        start_result = self._run_adb_command(['shell', 'am', 'start', '-e', 'RunInSubThread', '-n', DRT_ACTIVITY_FULL_NAME])
+        if start_result.find('Exception') != -1:
+            self._log_error('Failed to start DumpRenderTree application. Exception:\n' + start_result)
+            return False
+
+        if not ChromiumAndroidDriver._loop_with_timeout(self._all_pipes_created, DRT_START_STOP_TIMEOUT_SECS):
+            return False
+
+        # Read back the shell prompt to ensure adb shell ready.
+        deadline = time.time() + DRT_START_STOP_TIMEOUT_SECS
+        self._server_process.start()
+        self._read_prompt(deadline)
+        self._log_debug('Interactive shell started')
+
+        # Start a process to read from the stdout fifo of the DumpRenderTree app and print to stdout.
+        self._log_debug('Redirecting stdout to ' + self._out_fifo_path)
+        self._read_stdout_process = self._port._server_process_constructor(
+            self._port, 'ReadStdout', self._adb_command + ['shell', 'cat', self._out_fifo_path])
+        self._read_stdout_process.start()
+
+        # Start a process to read from the stderr fifo of the DumpRenderTree app and print to stdout.
+        self._log_debug('Redirecting stderr to ' + self._err_fifo_path)
+        self._read_stderr_process = self._port._server_process_constructor(
+            self._port, 'ReadStderr', self._adb_command + ['shell', 'cat', self._err_fifo_path])
+        self._read_stderr_process.start()
+
+        self._log_debug('Redirecting stdin to ' + self._in_fifo_path)
+        self._server_process.write('cat >%s\n' % self._in_fifo_path)
+
+        # Combine the stdout and stderr pipes into self._server_process.
+        self._server_process.replace_outputs(self._read_stdout_process._proc.stdout, self._read_stderr_process._proc.stdout)
+
+        def deadlock_detector(processes, normal_startup_event):
+            if not ChromiumAndroidDriver._loop_with_timeout(lambda: normal_startup_event.is_set(), DRT_START_STOP_TIMEOUT_SECS):
+                # If normal_startup_event is not set in time, the main thread must be blocked at
+                # reading/writing the fifo. Kill the fifo reading/writing processes to let the
+                # main thread escape from the deadlocked state. After that, the main thread will
+                # treat this as a crash.
+                self._log_error('Deadlock detected. Processes killed.')
+                for i in processes:
+                    i.kill()
+
+        # Start a thread to kill the pipe reading/writing processes on deadlock of the fifos during startup.
+        normal_startup_event = threading.Event()
+        threading.Thread(name='DeadlockDetector', target=deadlock_detector,
+                         args=([self._server_process, self._read_stdout_process, self._read_stderr_process], normal_startup_event)).start()
+
+        output = ''
+        line = self._server_process.read_stdout_line(deadline)
+        while not self._server_process.timed_out and not self.has_crashed() and line.rstrip() != '#READY':
+            output += line
+            line = self._server_process.read_stdout_line(deadline)
+
+        if self._server_process.timed_out and not self.has_crashed():
+            # DumpRenderTree crashes during startup, or when the deadlock detector detected
+            # deadlock and killed the fifo reading/writing processes.
+            _log.error('Failed to start DumpRenderTree: \n%s' % output)
+            return False
+        else:
+            # Inform the deadlock detector that the startup is successful without deadlock.
+            normal_startup_event.set()
+            return True
+
+    def stop(self):
+        self._run_adb_command(['shell', 'am', 'force-stop', DRT_APP_PACKAGE])
+
+        if self._read_stdout_process:
+            self._read_stdout_process.kill()
+            self._read_stdout_process = None
+
+        if self._read_stderr_process:
+            self._read_stderr_process.kill()
+            self._read_stderr_process = None
+
+        super(ChromiumAndroidDriver, self).stop()
+
+        if self._forwarder_process:
+            self._forwarder_process.kill()
+            self._forwarder_process = None
+
+        if self._has_setup:
+            if not ChromiumAndroidDriver._loop_with_timeout(self._remove_all_pipes, DRT_START_STOP_TIMEOUT_SECS):
+                raise AssertionError('Failed to remove fifo files. May be locked.')
+
+    def _command_from_driver_input(self, driver_input):
+        command = super(ChromiumAndroidDriver, self)._command_from_driver_input(driver_input)
+        if command.startswith('/'):
+            # Convert the host file path to a device file path. See comment of
+            # DEVICE_LAYOUT_TESTS_DIR for details.
+            # FIXME: what happens if command lies outside of the layout_tests_dir on the host?
+            command = DEVICE_LAYOUT_TESTS_DIR + self._port.relative_test_filename(command)
+        return command
+
+    def _read_prompt(self, deadline):
+        last_char = ''
+        while True:
+            current_char = self._server_process.read_stdout(deadline, 1)
+            if current_char == ' ':
+                if last_char in ('#', '$'):
+                    return
+            last_char = current_char
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_android_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_android_unittest.py
new file mode 100644
index 0000000..fce69c6
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_android_unittest.py
@@ -0,0 +1,294 @@
+# Copyright (C) 2012 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 optparse
+import StringIO
+import time
+import unittest
+import sys
+
+from webkitpy.common.system import executive_mock
+from webkitpy.common.system.executive_mock import MockExecutive2
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+
+from webkitpy.layout_tests.port import chromium_android
+from webkitpy.layout_tests.port import chromium_port_testcase
+from webkitpy.layout_tests.port import driver
+from webkitpy.layout_tests.port import driver_unittest
+from webkitpy.tool.mocktool import MockOptions
+
+class MockRunCommand(object):
+    def __init__(self):
+        self._mock_logcat = ''
+        self._mock_devices_output = ''
+        self._mock_devices = []
+        self._mock_ls_tombstones = ''
+
+    def mock_run_command_fn(self, args):
+        if args[0] != 'adb':
+            return ''
+        if args[1] == 'devices':
+            return self._mock_devices_output
+
+        assert len(args) > 3
+        assert args[1] == '-s'
+        assert args[2] in self._mock_devices
+        if args[3] == 'shell':
+            if args[4:] == ['ls', '-n', '/data/tombstones']:
+                return self._mock_ls_tombstones
+            elif args[4] == 'cat':
+                return args[5] + '\nmock_contents\n'
+        elif args[3] == 'logcat':
+            return self._mock_logcat
+        return ''
+
+    def mock_no_device(self):
+        self._mock_devices = []
+        self._mock_devices_output = 'List of devices attached'
+
+    def mock_one_device(self):
+        self._mock_devices = ['123456789ABCDEF0']
+        self._mock_devices_output = ('List of devices attached\n'
+                                     '%s\tdevice\n' % self._mock_devices[0])
+
+    def mock_two_devices(self):
+        self._mock_devices = ['123456789ABCDEF0', '23456789ABCDEF01']
+        self._mock_devices_output = ('* daemon not running. starting it now on port 5037 *'
+                                     '* daemon started successfully *'
+                                     'List of devices attached\n'
+                                     '%s\tdevice\n'
+                                     '%s\tdevice\n' % (self._mock_devices[0], self._mock_devices[1]))
+
+    def mock_no_tombstone_dir(self):
+        self._mock_ls_tombstones = '/data/tombstones: No such file or directory'
+
+    def mock_no_tombstone_file(self):
+        self._mock_ls_tombstones = ''
+
+    def mock_ten_tombstones(self):
+        self._mock_ls_tombstones = ('-rw------- 1000     1000       218643 2012-04-26 18:15 tombstone_00\n'
+                                    '-rw------- 1000     1000       241695 2012-04-26 18:15 tombstone_01\n'
+                                    '-rw------- 1000     1000       219472 2012-04-26 18:16 tombstone_02\n'
+                                    '-rw------- 1000     1000        45316 2012-04-27 16:33 tombstone_03\n'
+                                    '-rw------- 1000     1000        82022 2012-04-23 16:57 tombstone_04\n'
+                                    '-rw------- 1000     1000        82015 2012-04-23 16:57 tombstone_05\n'
+                                    '-rw------- 1000     1000        81974 2012-04-23 16:58 tombstone_06\n'
+                                    '-rw------- 1000     1000       237409 2012-04-26 17:41 tombstone_07\n'
+                                    '-rw------- 1000     1000       276089 2012-04-26 18:15 tombstone_08\n'
+                                    '-rw------- 1000     1000       219618 2012-04-26 18:15 tombstone_09\n')
+
+    def mock_logcat(self, content):
+        self._mock_logcat = content
+
+
+class ChromiumAndroidPortTest(chromium_port_testcase.ChromiumPortTestCase):
+    port_name = 'chromium-android'
+    port_maker = chromium_android.ChromiumAndroidPort
+
+    def make_port(self, **kwargs):
+        port = super(ChromiumAndroidPortTest, self).make_port(**kwargs)
+        self.mock_run_command = MockRunCommand()
+        self.mock_run_command.mock_one_device()
+        port._executive = MockExecutive2(run_command_fn=self.mock_run_command.mock_run_command_fn)
+        return port
+
+    def test_attributes(self):
+        port = self.make_port()
+        self.assertEquals(port.baseline_path(), port._webkit_baseline_path('chromium-android'))
+
+    def test_default_timeout_ms(self):
+        self.assertEquals(self.make_port(options=optparse.Values({'configuration': 'Release'})).default_timeout_ms(), 10000)
+        self.assertEquals(self.make_port(options=optparse.Values({'configuration': 'Debug'})).default_timeout_ms(), 10000)
+
+    def test_expectations_files(self):
+        # FIXME: override this test temporarily while we're still upstreaming the android port and
+        # using a custom expectations file.
+        pass
+
+    def test_get_devices_no_device(self):
+        port = self.make_port()
+        self.mock_run_command.mock_no_device()
+        self.assertRaises(AssertionError, port._get_devices)
+
+    def test_get_devices_one_device(self):
+        port = self.make_port()
+        self.mock_run_command.mock_one_device()
+        self.assertEquals(self.mock_run_command._mock_devices, port._get_devices())
+        self.assertEquals(1, port.default_child_processes())
+
+    def test_get_devices_two_devices(self):
+        port = self.make_port()
+        self.mock_run_command.mock_two_devices()
+        self.assertEquals(self.mock_run_command._mock_devices, port._get_devices())
+        self.assertEquals(2, port.default_child_processes())
+
+    def test_get_device_serial_no_device(self):
+        port = self.make_port()
+        self.mock_run_command.mock_no_device()
+        self.assertRaises(AssertionError, port._get_device_serial, 0)
+
+    def test_get_device_serial_one_device(self):
+        port = self.make_port()
+        self.mock_run_command.mock_one_device()
+        self.assertEquals(self.mock_run_command._mock_devices[0], port._get_device_serial(0))
+        self.assertRaises(AssertionError, port._get_device_serial, 1)
+
+    def test_get_device_serial_two_devices(self):
+        port = self.make_port()
+        self.mock_run_command.mock_two_devices()
+        self.assertEquals(self.mock_run_command._mock_devices[0], port._get_device_serial(0))
+        self.assertEquals(self.mock_run_command._mock_devices[1], port._get_device_serial(1))
+        self.assertRaises(AssertionError, port._get_device_serial, 2)
+
+    def test_must_require_http_server(self):
+        port = self.make_port()
+        self.assertEquals(port.requires_http_server(), True)
+
+
+class ChromiumAndroidDriverTest(unittest.TestCase):
+    def setUp(self):
+        self.mock_run_command = MockRunCommand()
+        self.mock_run_command.mock_one_device()
+        self.port = chromium_android.ChromiumAndroidPort(
+                MockSystemHost(executive=MockExecutive2(run_command_fn=self.mock_run_command.mock_run_command_fn)),
+                'chromium-android')
+        self.driver = chromium_android.ChromiumAndroidDriver(self.port, worker_number=0, pixel_tests=True)
+
+    def test_get_last_stacktrace(self):
+        self.mock_run_command.mock_no_tombstone_dir()
+        self.assertEquals(self.driver._get_last_stacktrace(), '')
+
+        self.mock_run_command.mock_no_tombstone_file()
+        self.assertEquals(self.driver._get_last_stacktrace(), '')
+
+        self.mock_run_command.mock_ten_tombstones()
+        self.assertEquals(self.driver._get_last_stacktrace(),
+                          '-rw------- 1000 1000 45316 2012-04-27 16:33 tombstone_03\n'
+                          '/data/tombstones/tombstone_03\nmock_contents\n')
+
+    def test_get_crash_log(self):
+        self.mock_run_command.mock_logcat('logcat contents\n')
+        self.mock_run_command.mock_ten_tombstones()
+        self.driver._crashed_process_name = 'foo'
+        self.driver._crashed_pid = 1234
+        self.assertEquals(self.driver._get_crash_log('out bar\nout baz\n', 'err bar\nerr baz\n', newer_than=None),
+            ('err bar\n'
+             'err baz\n'
+             '********* [123456789ABCDEF0] Tombstone file:\n'
+             '-rw------- 1000 1000 45316 2012-04-27 16:33 tombstone_03\n'
+             '/data/tombstones/tombstone_03\n'
+             'mock_contents\n',
+             u'crash log for foo (pid 1234):\n'
+             u'STDOUT: out bar\n'
+             u'STDOUT: out baz\n'
+             u'STDOUT: ********* [123456789ABCDEF0] Logcat:\n'
+             u'STDOUT: logcat contents\n'
+             u'STDERR: err bar\n'
+             u'STDERR: err baz\n'
+             u'STDERR: ********* [123456789ABCDEF0] Tombstone file:\n'
+             u'STDERR: -rw------- 1000 1000 45316 2012-04-27 16:33 tombstone_03\n'
+             u'STDERR: /data/tombstones/tombstone_03\n'
+             u'STDERR: mock_contents\n'))
+
+        self.driver._crashed_process_name = None
+        self.driver._crashed_pid = None
+        self.assertEquals(self.driver._get_crash_log(None, None, newer_than=None),
+            ('********* [123456789ABCDEF0] Tombstone file:\n'
+             '-rw------- 1000 1000 45316 2012-04-27 16:33 tombstone_03\n'
+             '/data/tombstones/tombstone_03\n'
+             'mock_contents\n',
+             u'crash log for <unknown process name> (pid <unknown>):\n'
+             u'STDOUT: ********* [123456789ABCDEF0] Logcat:\n'
+             u'STDOUT: logcat contents\n'
+             u'STDERR: ********* [123456789ABCDEF0] Tombstone file:\n'
+             u'STDERR: -rw------- 1000 1000 45316 2012-04-27 16:33 tombstone_03\n'
+             u'STDERR: /data/tombstones/tombstone_03\n'
+             u'STDERR: mock_contents\n'))
+
+    def test_cmd_line(self):
+        cmd_line = self.driver.cmd_line(True, ['anything'])
+        self.assertEquals(['adb', '-s', self.mock_run_command._mock_devices[0], 'shell'], cmd_line)
+
+    def test_drt_cmd_line(self):
+        cmd_line = self.driver._drt_cmd_line(True, ['--a'])
+        self.assertTrue('--a' in cmd_line)
+        self.assertTrue('--create-stdin-fifo' in cmd_line)
+        self.assertTrue('--separate-stderr-fifo' in cmd_line)
+
+    def test_read_prompt(self):
+        self.driver._server_process = driver_unittest.MockServerProcess(lines=['root@android:/ # '])
+        self.assertEquals(self.driver._read_prompt(time.time() + 1), None)
+        self.driver._server_process = driver_unittest.MockServerProcess(lines=['$ '])
+        self.assertEquals(self.driver._read_prompt(time.time() + 1), None)
+
+    def test_command_from_driver_input(self):
+        driver_input = driver.DriverInput('foo/bar/test.html', 10, 'checksum', True)
+        expected_command = "/data/local/tmp/third_party/WebKit/LayoutTests/foo/bar/test.html'--pixel-test'checksum\n"
+        if (sys.platform != "cygwin"):
+            self.assertEquals(self.driver._command_from_driver_input(driver_input), expected_command)
+
+        driver_input = driver.DriverInput('http/tests/foo/bar/test.html', 10, 'checksum', True)
+        expected_command = "http://127.0.0.1:8000/foo/bar/test.html'--pixel-test'checksum\n"
+        self.assertEquals(self.driver._command_from_driver_input(driver_input), expected_command)
+
+
+class ChromiumAndroidDriverTwoDriversTest(unittest.TestCase):
+    def test_two_drivers(self):
+        mock_run_command = MockRunCommand()
+        mock_run_command.mock_two_devices()
+        port = chromium_android.ChromiumAndroidPort(
+                MockSystemHost(executive=MockExecutive2(run_command_fn=mock_run_command.mock_run_command_fn)),
+                'chromium-android')
+        driver0 = chromium_android.ChromiumAndroidDriver(port, worker_number=0, pixel_tests=True)
+        driver1 = chromium_android.ChromiumAndroidDriver(port, worker_number=1, pixel_tests=True)
+
+        cmd_line0 = driver0.cmd_line(True, ['anything'])
+        self.assertEquals(['adb', '-s', mock_run_command._mock_devices[0], 'shell'], cmd_line0)
+
+        cmd_line1 = driver1.cmd_line(True, ['anything'])
+        self.assertEquals(['adb', '-s', mock_run_command._mock_devices[1], 'shell'], cmd_line1)
+
+
+class ChromiumAndroidTwoPortsTest(unittest.TestCase):
+    def test_options_with_two_ports(self):
+        options = MockOptions(additional_drt_flag=['--foo=bar', '--foo=baz'])
+        mock_run_command = MockRunCommand()
+        mock_run_command.mock_two_devices()
+        port0 = chromium_android.ChromiumAndroidPort(
+                MockSystemHost(executive=MockExecutive2(run_command_fn=mock_run_command.mock_run_command_fn)),
+                'chromium-android', options=options)
+        port1 = chromium_android.ChromiumAndroidPort(
+                MockSystemHost(executive=MockExecutive2(run_command_fn=mock_run_command.mock_run_command_fn)),
+                'chromium-android', options=options)
+        cmd_line = port1.driver_cmd_line()
+        self.assertEquals(cmd_line.count('--encode-binary'), 1)
+        self.assertEquals(cmd_line.count('--enable-hardware-gpu'), 1)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_linux.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_linux.py
new file mode 100644
index 0000000..7c37fd1
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_linux.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import logging
+
+from webkitpy.layout_tests.port import chromium
+from webkitpy.layout_tests.port import config
+
+
+_log = logging.getLogger(__name__)
+
+
+class ChromiumLinuxPort(chromium.ChromiumPort):
+    port_name = 'chromium-linux'
+
+    SUPPORTED_ARCHITECTURES = ('x86', 'x86_64')
+
+    FALLBACK_PATHS = {
+        'x86_64': [
+            'chromium-linux',
+            'chromium-win',
+            'chromium',
+        ],
+        'x86': [
+            'chromium-linux-x86',
+            'chromium-linux',
+            'chromium-win',
+            'chromium',
+        ],
+    }
+
+    DEFAULT_BUILD_DIRECTORIES = ('sconsbuild', 'out')
+
+    @classmethod
+    def _determine_driver_path_statically(cls, host, options):
+        config_object = config.Config(host.executive, host.filesystem)
+        build_directory = getattr(options, 'build_directory', None)
+        webkit_base = config_object.path_from_webkit_base()
+        chromium_base = cls._chromium_base_dir(host.filesystem)
+        if hasattr(options, 'configuration') and options.configuration:
+            configuration = options.configuration
+        else:
+            configuration = config_object.default_configuration()
+        return cls._static_build_path(host.filesystem, build_directory, chromium_base, webkit_base, configuration, ['DumpRenderTree'])
+
+    @staticmethod
+    def _determine_architecture(filesystem, executive, driver_path):
+        file_output = ''
+        if filesystem.exists(driver_path):
+            # The --dereference flag tells file to follow symlinks
+            file_output = executive.run_command(['file', '--dereference', driver_path], return_stderr=True)
+
+        if 'ELF 32-bit LSB executable' in file_output:
+            return 'x86'
+        if 'ELF 64-bit LSB executable' in file_output:
+            return 'x86_64'
+        if file_output:
+            _log.warning('Could not determine architecture from "file" output: %s' % file_output)
+
+        # We don't know what the architecture is; default to 'x86' because
+        # maybe we're rebaselining and the binary doesn't actually exist,
+        # or something else weird is going on. It's okay to do this because
+        # if we actually try to use the binary, check_build() should fail.
+        return 'x86_64'
+
+    @classmethod
+    def determine_full_port_name(cls, host, options, port_name):
+        if port_name.endswith('-linux'):
+            return port_name + '-' + cls._determine_architecture(host.filesystem, host.executive, cls._determine_driver_path_statically(host, options))
+        return port_name
+
+    def __init__(self, host, port_name, **kwargs):
+        chromium.ChromiumPort.__init__(self, host, port_name, **kwargs)
+        (base, arch) = port_name.rsplit('-', 1)
+        assert base == 'chromium-linux'
+        assert arch in self.SUPPORTED_ARCHITECTURES
+        assert port_name in ('chromium-linux', 'chromium-linux-x86', 'chromium-linux-x86_64')
+        self._version = 'lucid'  # We only support lucid right now.
+        self._architecture = arch
+
+    def default_baseline_search_path(self):
+        port_names = self.FALLBACK_PATHS[self._architecture]
+        return map(self._webkit_baseline_path, port_names)
+
+    def _modules_to_search_for_symbols(self):
+        return [self._build_path('libffmpegsumo.so')]
+
+    def check_build(self, needs_http):
+        result = chromium.ChromiumPort.check_build(self, needs_http)
+        if not result:
+            _log.error('For complete Linux build requirements, please see:')
+            _log.error('')
+            _log.error('    http://code.google.com/p/chromium/wiki/LinuxBuildInstructions')
+        return result
+
+    def operating_system(self):
+        return 'linux'
+
+    #
+    # PROTECTED METHODS
+    #
+
+    def _check_apache_install(self):
+        result = self._check_file_exists(self._path_to_apache(), "apache2")
+        result = self._check_file_exists(self._path_to_apache_config_file(), "apache2 config file") and result
+        if not result:
+            _log.error('    Please install using: "sudo apt-get install apache2 libapache2-mod-php5"')
+            _log.error('')
+        return result
+
+    def _check_lighttpd_install(self):
+        result = self._check_file_exists(
+            self._path_to_lighttpd(), "LigHTTPd executable")
+        result = self._check_file_exists(self._path_to_lighttpd_php(), "PHP CGI executable") and result
+        result = self._check_file_exists(self._path_to_lighttpd_modules(), "LigHTTPd modules") and result
+        if not result:
+            _log.error('    Please install using: "sudo apt-get install lighttpd php5-cgi"')
+            _log.error('')
+        return result
+
+    def _wdiff_missing_message(self):
+        return 'wdiff is not installed; please install using "sudo apt-get install wdiff"'
+
+    def _path_to_apache(self):
+        if self._is_redhat_based():
+            return '/usr/sbin/httpd'
+        else:
+            return '/usr/sbin/apache2'
+
+    def _path_to_lighttpd(self):
+        return "/usr/sbin/lighttpd"
+
+    def _path_to_lighttpd_modules(self):
+        return "/usr/lib/lighttpd"
+
+    def _path_to_lighttpd_php(self):
+        return "/usr/bin/php-cgi"
+
+    def _path_to_driver(self, configuration=None):
+        binary_name = self.driver_name()
+        return self._build_path_with_configuration(configuration, binary_name)
+
+    def _path_to_helper(self):
+        return None
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_linux_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_linux_unittest.py
new file mode 100644
index 0000000..169c2f4
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_linux_unittest.py
@@ -0,0 +1,118 @@
+# Copyright (C) 2011 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.system import executive_mock
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+from webkitpy.tool.mocktool import MockOptions
+
+from webkitpy.layout_tests.port import chromium_linux
+from webkitpy.layout_tests.port import chromium_port_testcase
+
+
+class ChromiumLinuxPortTest(chromium_port_testcase.ChromiumPortTestCase):
+    port_name = 'chromium-linux'
+    port_maker = chromium_linux.ChromiumLinuxPort
+
+    def assert_architecture(self, port_name=None, file_output=None, expected_architecture=None):
+        host = MockSystemHost()
+        host.filesystem.exists = lambda x: 'DumpRenderTree' in x
+        if file_output:
+            host.executive = executive_mock.MockExecutive2(file_output)
+
+        port = self.make_port(host, port_name=port_name)
+        self.assertEquals(port.architecture(), expected_architecture)
+        if expected_architecture == 'x86':
+            self.assertTrue(port.baseline_path().endswith('chromium-linux-x86'))
+            self.assertTrue(port.baseline_search_path()[0].endswith('chromium-linux-x86'))
+            self.assertTrue(port.baseline_search_path()[1].endswith('chromium-linux'))
+        else:
+            self.assertTrue(port.baseline_path().endswith('chromium-linux'))
+            self.assertTrue(port.baseline_search_path()[0].endswith('chromium-linux'))
+
+    def test_architectures(self):
+        self.assert_architecture(port_name='chromium-linux-x86',
+                                 expected_architecture='x86')
+        self.assert_architecture(port_name='chromium-linux-x86_64',
+                                 expected_architecture='x86_64')
+        self.assert_architecture(file_output='ELF 32-bit LSB executable',
+                                 expected_architecture='x86')
+        self.assert_architecture(file_output='ELF 64-bit LSB executable',
+                                 expected_architecture='x86_64')
+
+    def test_check_illegal_port_names(self):
+        # FIXME: Check that, for now, these are illegal port names.
+        # Eventually we should be able to do the right thing here.
+        self.assertRaises(AssertionError, chromium_linux.ChromiumLinuxPort, MockSystemHost(), port_name='chromium-x86-linux')
+
+    def test_determine_architecture_fails(self):
+        # Test that we default to 'x86' if the driver doesn't exist.
+        port = self.make_port()
+        self.assertEquals(port.architecture(), 'x86_64')
+
+        # Test that we default to 'x86' on an unknown architecture.
+        host = MockSystemHost()
+        host.filesystem.exists = lambda x: True
+        host.executive = executive_mock.MockExecutive2('win32')
+        port = self.make_port(host=host)
+        self.assertEquals(port.architecture(), 'x86_64')
+
+        # Test that we raise errors if something weird happens.
+        host.executive = executive_mock.MockExecutive2(exception=AssertionError)
+        self.assertRaises(AssertionError, chromium_linux.ChromiumLinuxPort, host, self.port_name)
+
+    def test_operating_system(self):
+        self.assertEqual('linux', self.make_port().operating_system())
+
+    def test_build_path(self):
+        # Test that optional paths are used regardless of whether they exist.
+        options = MockOptions(configuration='Release', build_directory='/foo')
+        self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out/Release'], '/foo/Release')
+
+        # Test that optional relative paths are returned unmodified.
+        options = MockOptions(configuration='Release', build_directory='foo')
+        self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out/Release'], 'foo/Release')
+
+        # Test that we look in a chromium directory before the webkit directory.
+        options = MockOptions(configuration='Release', build_directory=None)
+        self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out/Release', '/mock-checkout/out/Release'], '/mock-checkout/Source/WebKit/chromium/out/Release')
+
+        # Test that we prefer the legacy dir over the new dir.
+        options = MockOptions(configuration='Release', build_directory=None)
+        self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/sconsbuild/Release', '/mock-checkout/Source/WebKit/chromium/out/Release'], '/mock-checkout/Source/WebKit/chromium/sconsbuild/Release')
+
+    def test_driver_name_option(self):
+        self.assertTrue(self.make_port()._path_to_driver().endswith('DumpRenderTree'))
+        self.assertTrue(self.make_port(options=MockOptions(driver_name='OtherDriver'))._path_to_driver().endswith('OtherDriver'))
+
+    def test_path_to_image_diff(self):
+        self.assertEquals(self.make_port()._path_to_image_diff(), '/mock-checkout/out/Release/ImageDiff')
+
+if __name__ == '__main__':
+    port_testcase.main()
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_mac.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_mac.py
new file mode 100644
index 0000000..21b7e31
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_mac.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Chromium Mac implementation of the Port interface."""
+
+import logging
+import signal
+
+from webkitpy.layout_tests.port import chromium
+
+
+_log = logging.getLogger(__name__)
+
+
+class ChromiumMacPort(chromium.ChromiumPort):
+    SUPPORTED_OS_VERSIONS = ('snowleopard', 'lion', 'mountainlion', 'future')
+    port_name = 'chromium-mac'
+
+    FALLBACK_PATHS = {
+        'snowleopard': [
+            'chromium-mac-snowleopard',
+            'chromium-mac-lion',
+            'chromium-mac',
+            'chromium',
+        ],
+        'lion': [
+            'chromium-mac-lion',
+            'chromium-mac',
+            'chromium',
+        ],
+        'mountainlion': [
+            'chromium-mac',
+            'chromium',
+        ],
+        'future': [
+            'chromium-mac',
+            'chromium',
+        ],
+    }
+
+    DEFAULT_BUILD_DIRECTORIES = ('xcodebuild', 'out')
+
+    @classmethod
+    def determine_full_port_name(cls, host, options, port_name):
+        if port_name.endswith('-mac'):
+            return port_name + '-' + host.platform.os_version
+        return port_name
+
+    def __init__(self, host, port_name, **kwargs):
+        chromium.ChromiumPort.__init__(self, host, port_name, **kwargs)
+        self._version = port_name[port_name.index('chromium-mac-') + len('chromium-mac-'):]
+        assert self._version in self.SUPPORTED_OS_VERSIONS
+
+    def _modules_to_search_for_symbols(self):
+        return [self._build_path('ffmpegsumo.so')]
+
+    def check_build(self, needs_http):
+        result = chromium.ChromiumPort.check_build(self, needs_http)
+        if not result:
+            _log.error('For complete Mac build requirements, please see:')
+            _log.error('')
+            _log.error('    http://code.google.com/p/chromium/wiki/MacBuildInstructions')
+
+        return result
+
+    def operating_system(self):
+        return 'mac'
+
+    def expectations_files(self):
+        # FIXME: This is a temporary hack while getting the 10.8 baselines up to date.
+        # See https://bugs.webkit.org/show_bug.cgi?id=99505
+        files = super(ChromiumMacPort, self).expectations_files()
+        if self.name() == 'chromium-mac-mountainlion':
+            files.append(self._filesystem.join(self._webkit_baseline_path(self.name()), 'TestExpectations'))
+        return files
+
+    #
+    # PROTECTED METHODS
+    #
+
+    def _lighttpd_path(self, *comps):
+        return self.path_from_chromium_base('third_party', 'lighttpd', 'mac', *comps)
+
+    def _wdiff_missing_message(self):
+        return 'wdiff is not installed; please install from MacPorts or elsewhere'
+
+    def _path_to_apache(self):
+        return '/usr/sbin/httpd'
+
+    def _path_to_apache_config_file(self):
+        return self._filesystem.join(self.layout_tests_dir(), 'http', 'conf', 'apache2-httpd.conf')
+
+    def _path_to_lighttpd(self):
+        return self._lighttpd_path('bin', 'lighttpd')
+
+    def _path_to_lighttpd_modules(self):
+        return self._lighttpd_path('lib')
+
+    def _path_to_lighttpd_php(self):
+        return self._lighttpd_path('bin', 'php-cgi')
+
+    def _path_to_driver(self, configuration=None):
+        # FIXME: make |configuration| happy with case-sensitive file systems.
+        return self._build_path_with_configuration(configuration, self.driver_name() + '.app', 'Contents', 'MacOS', self.driver_name())
+
+    def _path_to_helper(self):
+        binary_name = 'LayoutTestHelper'
+        return self._build_path(binary_name)
+
+    def _path_to_wdiff(self):
+        return 'wdiff'
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_mac_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_mac_unittest.py
new file mode 100644
index 0000000..b02af5c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_mac_unittest.py
@@ -0,0 +1,110 @@
+# 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.layout_tests.port import chromium_mac
+from webkitpy.layout_tests.port import chromium_port_testcase
+from webkitpy.tool.mocktool import MockOptions
+
+
+class ChromiumMacPortTest(chromium_port_testcase.ChromiumPortTestCase):
+    os_name = 'mac'
+    os_version = 'snowleopard'
+    port_name = 'chromium-mac'
+    port_maker = chromium_mac.ChromiumMacPort
+
+    def assert_name(self, port_name, os_version_string, expected):
+        port = self.make_port(os_version=os_version_string, port_name=port_name)
+        self.assertEquals(expected, port.name())
+
+    def test_versions(self):
+        self.assertTrue(self.make_port().name() in ('chromium-mac-snowleopard', 'chromium-mac-lion', 'chromium-mac-mountainlion', 'chromium-mac-future'))
+
+        self.assert_name(None, 'snowleopard', 'chromium-mac-snowleopard')
+        self.assert_name('chromium-mac', 'snowleopard', 'chromium-mac-snowleopard')
+        self.assert_name('chromium-mac-snowleopard', 'leopard', 'chromium-mac-snowleopard')
+        self.assert_name('chromium-mac-snowleopard', 'snowleopard', 'chromium-mac-snowleopard')
+
+        self.assert_name(None, 'lion', 'chromium-mac-lion')
+        self.assert_name(None, 'mountainlion', 'chromium-mac-mountainlion')
+        self.assert_name(None, 'future', 'chromium-mac-future')
+
+        self.assert_name('chromium-mac', 'lion', 'chromium-mac-lion')
+        self.assert_name('chromium-mac-future', 'snowleopard', 'chromium-mac-future')
+        self.assert_name('chromium-mac-future', 'lion', 'chromium-mac-future')
+        self.assert_name('chromium-mac-future', 'mountainlion', 'chromium-mac-future')
+
+        self.assertRaises(AssertionError, self.assert_name, None, 'tiger', 'should-raise-assertion-so-this-value-does-not-matter')
+
+    def test_baseline_path(self):
+        port = self.make_port(port_name='chromium-mac-snowleopard')
+        self.assertEquals(port.baseline_path(), port._webkit_baseline_path('chromium-mac-snowleopard'))
+
+        port = self.make_port(port_name='chromium-mac-lion')
+        self.assertEquals(port.baseline_path(), port._webkit_baseline_path('chromium-mac-lion'))
+
+        port = self.make_port(port_name='chromium-mac-mountainlion')
+        self.assertEquals(port.baseline_path(), port._webkit_baseline_path('chromium-mac'))
+
+        port = self.make_port(port_name='chromium-mac-future')
+        self.assertEquals(port.baseline_path(), port._webkit_baseline_path('chromium-mac'))
+
+    def test_operating_system(self):
+        self.assertEqual('mac', self.make_port().operating_system())
+
+    def test_build_path(self):
+        # Test that optional paths are used regardless of whether they exist.
+        options = MockOptions(configuration='Release', build_directory='/foo')
+        self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out/Release'], '/foo/Release')
+
+        # Test that optional relative paths are returned unmodified.
+        options = MockOptions(configuration='Release', build_directory='foo')
+        self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out/Release'], 'foo/Release')
+
+        # Test that we look in a chromium directory before the webkit directory.
+        options = MockOptions(configuration='Release', build_directory=None)
+        self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out/Release', '/mock-checkout/out/Release'], '/mock-checkout/Source/WebKit/chromium/out/Release')
+
+        # Test that we prefer the legacy dir over the new dir.
+        options = MockOptions(configuration='Release', build_directory=None)
+        self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/xcodebuild/Release', '/mock-checkout/Source/WebKit/chromium/out/Release'], '/mock-checkout/Source/WebKit/chromium/xcodebuild/Release')
+
+    def test_driver_name_option(self):
+        self.assertTrue(self.make_port()._path_to_driver().endswith('DumpRenderTree'))
+        self.assertTrue(self.make_port(options=MockOptions(driver_name='OtherDriver'))._path_to_driver().endswith('OtherDriver'))
+
+    def test_path_to_image_diff(self):
+        self.assertEquals(self.make_port()._path_to_image_diff(), '/mock-checkout/out/Release/ImageDiff')
+
+    def test_ml_expectations(self):
+        self.assertTrue(self.make_port(port_name='chromium-mac-mountainlion').expectations_files()[-1].endswith('-mountainlion/TestExpectations'))
+        self.assertFalse(self.make_port(port_name='chromium-mac-lion').expectations_files()[-1].endswith('-mountainlion/TestExpectations'))
+
+if __name__ == '__main__':
+    port_testcase.main()
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_port_testcase.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_port_testcase.py
new file mode 100644
index 0000000..a082a13
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_port_testcase.py
@@ -0,0 +1,223 @@
+# 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.system import logtesting
+from webkitpy.common.system.executive_mock import MockExecutive2
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+from webkitpy.tool.mocktool import MockOptions
+
+import chromium_android
+import chromium_linux
+import chromium_mac
+import chromium_win
+
+from webkitpy.layout_tests.models.test_configuration import TestConfiguration
+from webkitpy.layout_tests.port import port_testcase
+
+
+class ChromiumPortTestCase(port_testcase.PortTestCase):
+
+    def test_check_build(self):
+        port = self.make_port()
+        port.check_build(needs_http=True)
+
+    def test_default_max_locked_shards(self):
+        port = self.make_port()
+        port.default_child_processes = lambda: 16
+        self.assertEquals(port.default_max_locked_shards(), 4)
+        port.default_child_processes = lambda: 2
+        self.assertEquals(port.default_max_locked_shards(), 1)
+
+    def test_default_timeout_ms(self):
+        self.assertEquals(self.make_port(options=MockOptions(configuration='Release')).default_timeout_ms(), 6000)
+        self.assertEquals(self.make_port(options=MockOptions(configuration='Debug')).default_timeout_ms(), 12000)
+
+    def test_default_pixel_tests(self):
+        self.assertEquals(self.make_port().default_pixel_tests(), True)
+
+    def test_missing_symbol_to_skipped_tests(self):
+        # Test that we get the chromium skips and not the webkit default skips
+        port = self.make_port()
+        skip_dict = port._missing_symbol_to_skipped_tests()
+        self.assertTrue('ff_mp3_decoder' in skip_dict)
+        self.assertFalse('WebGLShader' in skip_dict)
+
+    def test_all_test_configurations(self):
+        """Validate the complete set of configurations this port knows about."""
+        port = self.make_port()
+        self.assertEquals(set(port.all_test_configurations()), set([
+            TestConfiguration('icecreamsandwich', 'x86', 'debug'),
+            TestConfiguration('icecreamsandwich', 'x86', 'release'),
+            TestConfiguration('snowleopard', 'x86', 'debug'),
+            TestConfiguration('snowleopard', 'x86', 'release'),
+            TestConfiguration('lion', 'x86', 'debug'),
+            TestConfiguration('lion', 'x86', 'release'),
+            TestConfiguration('mountainlion', 'x86', 'debug'),
+            TestConfiguration('mountainlion', 'x86', 'release'),
+            TestConfiguration('xp', 'x86', 'debug'),
+            TestConfiguration('xp', 'x86', 'release'),
+            TestConfiguration('win7', 'x86', 'debug'),
+            TestConfiguration('win7', 'x86', 'release'),
+            TestConfiguration('lucid', 'x86', 'debug'),
+            TestConfiguration('lucid', 'x86', 'release'),
+            TestConfiguration('lucid', 'x86_64', 'debug'),
+            TestConfiguration('lucid', 'x86_64', 'release'),
+        ]))
+
+    class TestMacPort(chromium_mac.ChromiumMacPort):
+        def __init__(self, options=None):
+            options = options or MockOptions()
+            chromium_mac.ChromiumMacPort.__init__(self, MockSystemHost(os_name='mac', os_version='leopard'), 'chromium-mac-leopard', options=options)
+
+        def default_configuration(self):
+            self.default_configuration_called = True
+            return 'default'
+
+    class TestAndroidPort(chromium_android.ChromiumAndroidPort):
+        def __init__(self, options=None):
+            options = options or MockOptions()
+            chromium_win.ChromiumAndroidPort.__init__(self, MockSystemHost(os_name='android', os_version='icecreamsandwich'), 'chromium-android', options=options)
+
+        def default_configuration(self):
+            self.default_configuration_called = True
+            return 'default'
+
+    class TestLinuxPort(chromium_linux.ChromiumLinuxPort):
+        def __init__(self, options=None):
+            options = options or MockOptions()
+            chromium_linux.ChromiumLinuxPort.__init__(self, MockSystemHost(os_name='linux', os_version='lucid'), 'chromium-linux-x86', options=options)
+
+        def default_configuration(self):
+            self.default_configuration_called = True
+            return 'default'
+
+    class TestWinPort(chromium_win.ChromiumWinPort):
+        def __init__(self, options=None):
+            options = options or MockOptions()
+            chromium_win.ChromiumWinPort.__init__(self, MockSystemHost(os_name='win', os_version='xp'), 'chromium-win-xp', options=options)
+
+        def default_configuration(self):
+            self.default_configuration_called = True
+            return 'default'
+
+    def test_default_configuration(self):
+        mock_options = MockOptions()
+        port = ChromiumPortTestCase.TestLinuxPort(options=mock_options)
+        self.assertEquals(mock_options.configuration, 'default')
+        self.assertTrue(port.default_configuration_called)
+
+        mock_options = MockOptions(configuration=None)
+        port = ChromiumPortTestCase.TestLinuxPort(mock_options)
+        self.assertEquals(mock_options.configuration, 'default')
+        self.assertTrue(port.default_configuration_called)
+
+    def test_diff_image(self):
+        class TestPort(ChromiumPortTestCase.TestLinuxPort):
+            def _path_to_image_diff(self):
+                return "/path/to/image_diff"
+
+        port = ChromiumPortTestCase.TestLinuxPort()
+        mock_image_diff = "MOCK Image Diff"
+
+        def mock_run_command(args):
+            port._filesystem.write_binary_file(args[4], mock_image_diff)
+            return 1
+
+        # Images are different.
+        port._executive = MockExecutive2(run_command_fn=mock_run_command)
+        self.assertEquals(mock_image_diff, port.diff_image("EXPECTED", "ACTUAL")[0])
+
+        # Images are the same.
+        port._executive = MockExecutive2(exit_code=0)
+        self.assertEquals(None, port.diff_image("EXPECTED", "ACTUAL")[0])
+
+        # There was some error running image_diff.
+        port._executive = MockExecutive2(exit_code=2)
+        exception_raised = False
+        try:
+            port.diff_image("EXPECTED", "ACTUAL")
+        except ValueError, e:
+            exception_raised = True
+        self.assertFalse(exception_raised)
+
+    def test_diff_image_crashed(self):
+        port = ChromiumPortTestCase.TestLinuxPort()
+        port._executive = MockExecutive2(exit_code=2)
+        self.assertEquals(port.diff_image("EXPECTED", "ACTUAL"), (None, 0, 'image diff returned an exit code of 2'))
+
+    def test_expectations_files(self):
+        port = self.make_port()
+        port.port_name = 'chromium'
+
+        expectations_path = port.path_to_test_expectations_file()
+        chromium_overrides_path = port.path_from_chromium_base(
+            'webkit', 'tools', 'layout_tests', 'test_expectations.txt')
+        skia_overrides_path = port.path_from_chromium_base(
+            'skia', 'skia_test_expectations.txt')
+
+        port._filesystem.write_text_file(skia_overrides_path, 'dummay text')
+
+        port._options.builder_name = 'DUMMY_BUILDER_NAME'
+        self.assertEquals(port.expectations_files(), [expectations_path, skia_overrides_path, chromium_overrides_path])
+
+        port._options.builder_name = 'builder (deps)'
+        self.assertEquals(port.expectations_files(), [expectations_path, skia_overrides_path, chromium_overrides_path])
+
+        # A builder which does NOT observe the Chromium test_expectations,
+        # but still observes the Skia test_expectations...
+        port._options.builder_name = 'builder'
+        self.assertEquals(port.expectations_files(), [expectations_path, skia_overrides_path])
+
+    def test_expectations_ordering(self):
+        # since we don't implement self.port_name in ChromiumPort.
+        pass
+
+
+class ChromiumPortLoggingTest(logtesting.LoggingTestCase):
+    def test_check_sys_deps(self):
+        port = ChromiumPortTestCase.TestLinuxPort()
+
+        # Success
+        port._executive = MockExecutive2(exit_code=0)
+        self.assertTrue(port.check_sys_deps(needs_http=False))
+
+        # Failure
+        port._executive = MockExecutive2(exit_code=1,
+            output='testing output failure')
+        self.assertFalse(port.check_sys_deps(needs_http=False))
+        self.assertLog([
+            'ERROR: System dependencies check failed.\n',
+            'ERROR: To override, invoke with --nocheck-sys-deps\n',
+            'ERROR: \n',
+            'ERROR: testing output failure\n'])
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_unittest.py
new file mode 100644
index 0000000..87de41c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_unittest.py
@@ -0,0 +1,69 @@
+# 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 StringIO
+import time
+import unittest
+
+from webkitpy.common.system import logtesting
+from webkitpy.common.system.executive_mock import MockExecutive2
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+from webkitpy.layout_tests.port.config_mock import MockConfig
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.mocktool import MockOptions
+
+import chromium
+import chromium_mac
+
+from webkitpy.layout_tests.port import chromium_port_testcase
+from webkitpy.layout_tests.port.driver import DriverInput
+
+
+class ChromiumPortLoggingTest(logtesting.LoggingTestCase):
+
+    # FIXME: put this someplace more useful
+    def test_check_sys_deps(self):
+        port = chromium_port_testcase.ChromiumPortTestCase.TestLinuxPort()
+
+        # Success
+        port._executive = MockExecutive2(exit_code=0)
+        self.assertTrue(port.check_sys_deps(needs_http=False))
+
+        # Failure
+        port._executive = MockExecutive2(exit_code=1,
+            output='testing output failure')
+        self.assertFalse(port.check_sys_deps(needs_http=False))
+        self.assertLog([
+            'ERROR: System dependencies check failed.\n',
+            'ERROR: To override, invoke with --nocheck-sys-deps\n',
+            'ERROR: \n',
+            'ERROR: testing output failure\n'])
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_win.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_win.py
new file mode 100755
index 0000000..3266c39
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_win.py
@@ -0,0 +1,157 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Chromium Win implementation of the Port interface."""
+
+import os
+import logging
+
+import chromium
+
+
+_log = logging.getLogger(__name__)
+
+
+class ChromiumWinPort(chromium.ChromiumPort):
+    port_name = 'chromium-win'
+
+    # FIXME: Figure out how to unify this with base.TestConfiguration.all_systems()?
+    SUPPORTED_VERSIONS = ('xp', 'win7')
+
+    FALLBACK_PATHS = {
+        'xp': [
+            'chromium-win-xp',
+            'chromium-win',
+            'chromium',
+        ],
+        'win7': [
+            'chromium-win',
+            'chromium',
+        ],
+    }
+
+    DEFAULT_BUILD_DIRECTORIES = ('build', 'out')
+
+    @classmethod
+    def determine_full_port_name(cls, host, options, port_name):
+        if port_name.endswith('-win'):
+            assert host.platform.is_win()
+            # We don't maintain separate baselines for vista, so we pretend it is win7.
+            if host.platform.os_version in ('vista', '7sp0', '7sp1', 'future'):
+                version = 'win7'
+            else:
+                version = host.platform.os_version
+            port_name = port_name + '-' + version
+        return port_name
+
+    def __init__(self, host, port_name, **kwargs):
+        chromium.ChromiumPort.__init__(self, host, port_name, **kwargs)
+        self._version = port_name[port_name.index('chromium-win-') + len('chromium-win-'):]
+        assert self._version in self.SUPPORTED_VERSIONS, "%s is not in %s" % (self._version, self.SUPPORTED_VERSIONS)
+
+    def setup_environ_for_server(self, server_name=None):
+        env = chromium.ChromiumPort.setup_environ_for_server(self, server_name)
+
+        # FIXME: lighttpd depends on some environment variable we're not whitelisting.
+        # We should add the variable to an explicit whitelist in base.Port.
+        # FIXME: This is a temporary hack to get the cr-win bot online until
+        # someone from the cr-win port can take a look.
+        for key, value in os.environ.items():
+            if key not in env:
+                env[key] = value
+
+        # Put the cygwin directory first in the path to find cygwin1.dll.
+        env["PATH"] = "%s;%s" % (self.path_from_chromium_base("third_party", "cygwin", "bin"), env["PATH"])
+        # Configure the cygwin directory so that pywebsocket finds proper
+        # python executable to run cgi program.
+        env["CYGWIN_PATH"] = self.path_from_chromium_base("third_party", "cygwin", "bin")
+        if self.get_option('register_cygwin'):
+            setup_mount = self.path_from_chromium_base("third_party", "cygwin", "setup_mount.bat")
+            self._executive.run_command([setup_mount])  # Paths are all absolute, so this does not require a cwd.
+        return env
+
+    def _modules_to_search_for_symbols(self):
+        # FIXME: we should return the path to the ffmpeg equivalents to detect if we have the mp3 and aac codecs installed.
+        # See https://bugs.webkit.org/show_bug.cgi?id=89706.
+        return []
+
+    def check_build(self, needs_http):
+        result = chromium.ChromiumPort.check_build(self, needs_http)
+        if not result:
+            _log.error('For complete Windows build requirements, please see:')
+            _log.error('')
+            _log.error('    http://dev.chromium.org/developers/how-tos/build-instructions-windows')
+        return result
+
+    def operating_system(self):
+        return 'win'
+
+    def relative_test_filename(self, filename):
+        path = filename[len(self.layout_tests_dir()) + 1:]
+        return path.replace('\\', '/')
+
+    #
+    # PROTECTED ROUTINES
+    #
+
+    def _uses_apache(self):
+        return False
+
+    def _lighttpd_path(self, *comps):
+        return self.path_from_chromium_base('third_party', 'lighttpd', 'win', *comps)
+
+    def _path_to_apache(self):
+        return self.path_from_chromium_base('third_party', 'cygwin', 'usr', 'sbin', 'httpd')
+
+    def _path_to_apache_config_file(self):
+        return self._filesystem.join(self.layout_tests_dir(), 'http', 'conf', 'cygwin-httpd.conf')
+
+    def _path_to_lighttpd(self):
+        return self._lighttpd_path('LightTPD.exe')
+
+    def _path_to_lighttpd_modules(self):
+        return self._lighttpd_path('lib')
+
+    def _path_to_lighttpd_php(self):
+        return self._lighttpd_path('php5', 'php-cgi.exe')
+
+    def _path_to_driver(self, configuration=None):
+        binary_name = '%s.exe' % self.driver_name()
+        return self._build_path_with_configuration(configuration, binary_name)
+
+    def _path_to_helper(self):
+        binary_name = 'LayoutTestHelper.exe'
+        return self._build_path(binary_name)
+
+    def _path_to_image_diff(self):
+        binary_name = 'ImageDiff.exe'
+        return self._build_path(binary_name)
+
+    def _path_to_wdiff(self):
+        return self.path_from_chromium_base('third_party', 'cygwin', 'bin', 'wdiff.exe')
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_win_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_win_unittest.py
new file mode 100644
index 0000000..dc184fc
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_win_unittest.py
@@ -0,0 +1,125 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import unittest
+
+from webkitpy.common.system import outputcapture
+from webkitpy.common.system.executive_mock import MockExecutive
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.layout_tests.port import chromium_port_testcase
+from webkitpy.layout_tests.port import chromium_win
+from webkitpy.tool.mocktool import MockOptions
+
+
+class ChromiumWinTest(chromium_port_testcase.ChromiumPortTestCase):
+    port_name = 'chromium-win'
+    port_maker = chromium_win.ChromiumWinPort
+    os_name = 'win'
+    os_version = 'xp'
+
+    def test_uses_apache(self):
+        self.assertFalse(self.make_port()._uses_apache())
+
+    def test_setup_environ_for_server(self):
+        port = self.make_port()
+        port._executive = MockExecutive(should_log=True)
+        output = outputcapture.OutputCapture()
+        # FIXME: This test should not use the real os.environ
+        orig_environ = os.environ.copy()
+        env = output.assert_outputs(self, port.setup_environ_for_server)
+        self.assertEqual(orig_environ["PATH"], os.environ["PATH"])
+        self.assertNotEqual(env["PATH"], os.environ["PATH"])
+
+    def test_setup_environ_for_server_cygpath(self):
+        port = self.make_port()
+        env = port.setup_environ_for_server(port.driver_name())
+        self.assertEquals(env['CYGWIN_PATH'], '/mock-checkout/Source/WebKit/chromium/third_party/cygwin/bin')
+
+    def test_setup_environ_for_server_register_cygwin(self):
+        port = self.make_port(options=MockOptions(register_cygwin=True, results_directory='/'))
+        port._executive = MockExecutive(should_log=True)
+        expected_stderr = "MOCK run_command: ['/mock-checkout/Source/WebKit/chromium/third_party/cygwin/setup_mount.bat'], cwd=None\n"
+        output = outputcapture.OutputCapture()
+        output.assert_outputs(self, port.setup_environ_for_server, expected_stderr=expected_stderr)
+
+    def assert_name(self, port_name, os_version_string, expected):
+        port = self.make_port(port_name=port_name, os_version=os_version_string)
+        self.assertEquals(expected, port.name())
+
+    def test_versions(self):
+        port = self.make_port()
+        self.assertTrue(port.name() in ('chromium-win-xp', 'chromium-win-win7'))
+
+        self.assert_name(None, 'xp', 'chromium-win-xp')
+        self.assert_name('chromium-win', 'xp', 'chromium-win-xp')
+        self.assert_name('chromium-win-xp', 'xp', 'chromium-win-xp')
+        self.assert_name('chromium-win-xp', '7sp0', 'chromium-win-xp')
+
+        self.assert_name(None, '7sp0', 'chromium-win-win7')
+        self.assert_name(None, 'vista', 'chromium-win-win7')
+        self.assert_name('chromium-win', '7sp0', 'chromium-win-win7')
+        self.assert_name('chromium-win-win7', 'xp', 'chromium-win-win7')
+        self.assert_name('chromium-win-win7', '7sp0', 'chromium-win-win7')
+        self.assert_name('chromium-win-win7', 'vista', 'chromium-win-win7')
+
+        self.assertRaises(AssertionError, self.assert_name, None, 'w2k', 'chromium-win-xp')
+
+    def test_baseline_path(self):
+        port = self.make_port(port_name='chromium-win-xp')
+        self.assertEquals(port.baseline_path(), port._webkit_baseline_path('chromium-win-xp'))
+
+        port = self.make_port(port_name='chromium-win-win7')
+        self.assertEquals(port.baseline_path(), port._webkit_baseline_path('chromium-win'))
+
+    def test_build_path(self):
+        # Test that optional paths are used regardless of whether they exist.
+        options = MockOptions(configuration='Release', build_directory='/foo')
+        self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out/Release'], '/foo/Release')
+
+        # Test that optional relative paths are returned unmodified.
+        options = MockOptions(configuration='Release', build_directory='foo')
+        self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out/Release'], 'foo/Release')
+
+        # Test that we look in a chromium directory before the webkit directory.
+        options = MockOptions(configuration='Release', build_directory=None)
+        self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out/Release', '/mock-checkout/out/Release'], '/mock-checkout/Source/WebKit/chromium/out/Release')
+
+        # Test that we prefer the legacy dir over the new dir.
+        options = MockOptions(configuration='Release', build_directory=None)
+        self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/build/Release', '/mock-checkout/Source/WebKit/chromium/out'], '/mock-checkout/Source/WebKit/chromium/build/Release')
+
+    def test_operating_system(self):
+        self.assertEqual('win', self.make_port().operating_system())
+
+    def test_driver_name_option(self):
+        self.assertTrue(self.make_port()._path_to_driver().endswith('DumpRenderTree.exe'))
+        self.assertTrue(self.make_port(options=MockOptions(driver_name='OtherDriver'))._path_to_driver().endswith('OtherDriver.exe'))
+
+    def test_path_to_image_diff(self):
+        self.assertEquals(self.make_port()._path_to_image_diff(), '/mock-checkout/out/Release/ImageDiff.exe')
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/config.py b/Tools/Scripts/webkitpy/layout_tests/port/config.py
new file mode 100644
index 0000000..dd8f331
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/config.py
@@ -0,0 +1,168 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Wrapper objects for WebKit-specific utility routines."""
+
+# FIXME: This file needs to be unified with common/checkout/scm.py and
+# common/config/ports.py .
+
+from webkitpy.common.system import logutils
+from webkitpy.common.system import executive
+
+
+_log = logutils.get_logger(__file__)
+
+#
+# FIXME: This is used to record if we've already hit the filesystem to look
+# for a default configuration. We cache this to speed up the unit tests,
+# but this can be reset with clear_cached_configuration(). This should be
+# replaced with us consistently using MockConfigs() for tests that don't
+# hit the filesystem at all and provide a reliable value.
+#
+_have_determined_configuration = False
+_configuration = "Release"
+
+
+def clear_cached_configuration():
+    global _have_determined_configuration, _configuration
+    _have_determined_configuration = False
+    _configuration = "Release"
+
+
+class Config(object):
+    _FLAGS_FROM_CONFIGURATIONS = {
+        "Debug": "--debug",
+        "Release": "--release",
+    }
+
+    def __init__(self, executive, filesystem, port_implementation=None):
+        self._executive = executive
+        self._filesystem = filesystem
+        self._webkit_base_dir = None
+        self._default_configuration = None
+        self._build_directories = {}
+        self._port_implementation = port_implementation
+
+    def build_directory(self, configuration):
+        """Returns the path to the build directory for the configuration."""
+        if configuration:
+            flags = ["--configuration", self.flag_for_configuration(configuration)]
+        else:
+            configuration = ""
+            flags = []
+
+        if self._port_implementation:
+            flags.append('--' + self._port_implementation)
+
+        if not self._build_directories.get(configuration):
+            args = ["perl", self.script_path("webkit-build-directory")] + flags
+            output = self._executive.run_command(args, cwd=self.webkit_base_dir(), return_stderr=False).rstrip()
+            parts = output.split("\n")
+            self._build_directories[configuration] = parts[0]
+
+            if len(parts) == 2:
+                default_configuration = parts[1][len(parts[0]):]
+                if default_configuration.startswith("/"):
+                    default_configuration = default_configuration[1:]
+                self._build_directories[default_configuration] = parts[1]
+
+        return self._build_directories[configuration]
+
+    def flag_for_configuration(self, configuration):
+        return self._FLAGS_FROM_CONFIGURATIONS[configuration]
+
+    def default_configuration(self):
+        """Returns the default configuration for the user.
+
+        Returns the value set by 'set-webkit-configuration', or "Release"
+        if that has not been set. This mirrors the logic in webkitdirs.pm."""
+        if not self._default_configuration:
+            self._default_configuration = self._determine_configuration()
+        if not self._default_configuration:
+            self._default_configuration = 'Release'
+        if self._default_configuration not in self._FLAGS_FROM_CONFIGURATIONS:
+            _log.warn("Configuration \"%s\" is not a recognized value.\n" % self._default_configuration)
+            _log.warn("Scripts may fail.  See 'set-webkit-configuration --help'.")
+        return self._default_configuration
+
+    def path_from_webkit_base(self, *comps):
+        return self._filesystem.join(self.webkit_base_dir(), *comps)
+
+    # FIXME: We should only have one implementation of this logic,
+    # if scm.find_checkout_root() is broken for Chromium, we should fix (or at least wrap) it!
+    def webkit_base_dir(self):
+        """Returns the absolute path to the top of the WebKit tree.
+
+        Raises an AssertionError if the top dir can't be determined."""
+        # Note: this code somewhat duplicates the code in
+        # scm.find_checkout_root(). However, that code only works if the top
+        # of the SCM repository also matches the top of the WebKit tree. The
+        # Chromium ports, for example, only check out subdirectories like
+        # Tools/Scripts, and so we still have to do additional work
+        # to find the top of the tree.
+        #
+        # This code will also work if there is no SCM system at all.
+        if not self._webkit_base_dir:
+            config_module_path = self._filesystem.path_to_module(self.__module__)
+            self._webkit_base_dir = config_module_path[0:config_module_path.find('Tools') - 1]
+        return self._webkit_base_dir
+
+    def script_path(self, script_name):
+        # This is intentionally relative. Callers should pass the checkout_root/webkit_base_dir to run_command as the cwd.
+        return self._filesystem.join("Tools", "Scripts", script_name)
+
+    def _determine_configuration(self):
+        # This mirrors the logic in webkitdirs.pm:determineConfiguration().
+        #
+        # FIXME: See the comment at the top of the file regarding unit tests
+        # and our use of global mutable static variables.
+        # FIXME: We should just @memoize this method and then this will only
+        # be read once per object lifetime (which should be sufficiently fast).
+        global _have_determined_configuration, _configuration
+        if not _have_determined_configuration:
+            contents = self._read_configuration()
+            if not contents:
+                contents = "Release"
+            if contents == "Deployment":
+                contents = "Release"
+            if contents == "Development":
+                contents = "Debug"
+            _configuration = contents
+            _have_determined_configuration = True
+        return _configuration
+
+    def _read_configuration(self):
+        try:
+            configuration_path = self._filesystem.join(self.build_directory(None), "Configuration")
+            if not self._filesystem.exists(configuration_path):
+                return None
+        except (OSError, executive.ScriptError):
+            return None
+
+        return self._filesystem.read_text_file(configuration_path).rstrip()
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/config_mock.py b/Tools/Scripts/webkitpy/layout_tests/port/config_mock.py
new file mode 100644
index 0000000..5476e4b
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/config_mock.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Wrapper objects for WebKit-specific utility routines."""
+
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+
+
+class MockConfig(object):
+    _FLAGS_FROM_CONFIGURATIONS = {
+        "Debug": "--debug",
+        "Release": "--release",
+    }
+
+    def __init__(self, filesystem=None, default_configuration='Release', port_implementation=None):
+        self._filesystem = filesystem or MockFileSystem()
+        self._default_configuration = default_configuration
+        self._port_implmentation = port_implementation
+
+    def flag_for_configuration(self, configuration):
+        return self._FLAGS_FROM_CONFIGURATIONS[configuration]
+
+    def build_directory(self, configuration):
+        return "/mock-build"
+
+    def build_dumprendertree(self, configuration):
+        return True
+
+    def default_configuration(self):
+        return self._default_configuration
+
+    def path_from_webkit_base(self, *comps):
+        # FIXME: This could use self._filesystem.join, but that doesn't handle empty lists.
+        return self.webkit_base_dir() + "/" + "/".join(list(comps))
+
+    def script_path(self, script_name):
+        # This is intentionally relative. Callers should pass the checkout_root/webkit_base_dir to run_command as the cwd.
+        return self._filesystem.join("Tools", "Scripts", script_name)
+
+    def webkit_base_dir(self):
+        return "/mock-checkout"
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/config_standalone.py b/Tools/Scripts/webkitpy/layout_tests/port/config_standalone.py
new file mode 100644
index 0000000..5b04831
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/config_standalone.py
@@ -0,0 +1,70 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""FIXME: This script is used by
+config_unittest.test_default_configuration__standalone() to read the
+default configuration to work around any possible caching / reset bugs. See
+https://bugs.webkit.org/show_bug?id=49360 for the motivation. We can remove
+this test when we remove the global configuration cache in config.py."""
+
+import os
+import unittest
+import sys
+
+
+# Ensure that webkitpy is in PYTHONPATH.
+this_dir = os.path.abspath(sys.path[0])
+up = os.path.dirname
+script_dir = up(up(up(this_dir)))
+if script_dir not in sys.path:
+    sys.path.append(script_dir)
+
+from webkitpy.common.system import executive
+from webkitpy.common.system import executive_mock
+from webkitpy.common.system import filesystem
+from webkitpy.common.system import filesystem_mock
+
+import config
+
+
+def main(argv=None):
+    if not argv:
+        argv = sys.argv
+
+    if len(argv) == 3 and argv[1] == '--mock':
+        e = executive_mock.MockExecutive2(output='foo\nfoo/%s' % argv[2])
+        fs = filesystem_mock.MockFileSystem({'foo/Configuration': argv[2]})
+    else:
+        e = executive.Executive()
+        fs = filesystem.FileSystem()
+
+    c = config.Config(e, fs)
+    print c.default_configuration()
+
+if __name__ == '__main__':
+    main()
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/config_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/config_unittest.py
new file mode 100644
index 0000000..96ba5ff
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/config_unittest.py
@@ -0,0 +1,188 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import sys
+import unittest
+
+from webkitpy.common.system.executive import Executive, ScriptError
+from webkitpy.common.system.executive_mock import MockExecutive, MockExecutive2
+from webkitpy.common.system.filesystem import FileSystem
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.outputcapture import OutputCapture
+
+import config
+
+
+class ConfigTest(unittest.TestCase):
+    def setUp(self):
+        config.clear_cached_configuration()
+
+    def tearDown(self):
+        config.clear_cached_configuration()
+
+    def make_config(self, output='', files=None, exit_code=0, exception=None, run_command_fn=None, stderr='', port_implementation=None):
+        e = MockExecutive2(output=output, exit_code=exit_code, exception=exception, run_command_fn=run_command_fn, stderr=stderr)
+        fs = MockFileSystem(files)
+        return config.Config(e, fs, port_implementation=port_implementation)
+
+    def assert_configuration(self, contents, expected):
+        # This tests that a configuration file containing
+        # _contents_ ends up being interpreted as _expected_.
+        output = 'foo\nfoo/%s' % contents
+        c = self.make_config(output, {'foo/Configuration': contents})
+        self.assertEqual(c.default_configuration(), expected)
+
+    def test_build_directory(self):
+        # --top-level
+        def mock_webkit_build_directory(arg_list):
+            if arg_list == ['--top-level']:
+                return '/WebKitBuild/'
+            elif arg_list == ['--configuration', '--debug']:
+                return '/WebKitBuild/Debug'
+            elif arg_list == ['--configuration', '--release']:
+                return '/WebKitBuild/Release'
+            elif arg_list == []:
+                return '/WebKitBuild/\n/WebKitBuild//Debug\n'
+            return 'Error'
+
+        def mock_run_command(arg_list):
+            if 'webkit-build-directory' in arg_list[1]:
+                return mock_webkit_build_directory(arg_list[2:])
+            return 'Error'
+
+        c = self.make_config(run_command_fn=mock_run_command)
+        self.assertEqual(c.build_directory(None), '/WebKitBuild/')
+
+        # Test again to check caching
+        self.assertEqual(c.build_directory(None), '/WebKitBuild/')
+
+        # Test other values
+        self.assertTrue(c.build_directory('Release').endswith('/Release'))
+        self.assertTrue(c.build_directory('Debug').endswith('/Debug'))
+        self.assertRaises(KeyError, c.build_directory, 'Unknown')
+
+        # Test that stderr output from webkit-build-directory won't mangle the build dir
+        c = self.make_config(output='/WebKitBuild/', stderr="mock stderr output from webkit-build-directory")
+        self.assertEqual(c.build_directory(None), '/WebKitBuild/')
+
+    def test_build_directory_passes_port_implementation(self):
+        def mock_run_command(arg_list):
+            self.assetEquals('--gtk' in arg_list)
+            return '/tmp'
+
+        c = self.make_config(run_command_fn=mock_run_command, port_implementation='gtk')
+
+    def test_default_configuration__release(self):
+        self.assert_configuration('Release', 'Release')
+
+    def test_default_configuration__debug(self):
+        self.assert_configuration('Debug', 'Debug')
+
+    def test_default_configuration__deployment(self):
+        self.assert_configuration('Deployment', 'Release')
+
+    def test_default_configuration__development(self):
+        self.assert_configuration('Development', 'Debug')
+
+    def test_default_configuration__notfound(self):
+        # This tests what happens if the default configuration file doesn't exist.
+        c = self.make_config(output='foo\nfoo/Release', files={'foo/Configuration': None})
+        self.assertEqual(c.default_configuration(), "Release")
+
+    def test_default_configuration__unknown(self):
+        # Ignore the warning about an unknown configuration value.
+        oc = OutputCapture()
+        oc.capture_output()
+        self.assert_configuration('Unknown', 'Unknown')
+        oc.restore_output()
+
+    def test_default_configuration__standalone(self):
+        # FIXME: This test runs a standalone python script to test
+        # reading the default configuration to work around any possible
+        # caching / reset bugs. See https://bugs.webkit.org/show_bug.cgi?id=49360
+        # for the motivation. We can remove this test when we remove the
+        # global configuration cache in config.py.
+        e = Executive()
+        fs = FileSystem()
+        c = config.Config(e, fs)
+        script = c.path_from_webkit_base('Tools', 'Scripts', 'webkitpy', 'layout_tests', 'port', 'config_standalone.py')
+
+        # Note: don't use 'Release' here, since that's the normal default.
+        expected = 'Debug'
+
+        # FIXME: Why are we running a python subprocess here??
+        args = [sys.executable, script, '--mock', expected]
+        actual = e.run_command(args).rstrip()
+        self.assertEqual(actual, expected)
+
+    def test_default_configuration__no_perl(self):
+        # We need perl to run webkit-build-directory to find out where the
+        # default configuration file is. See what happens if perl isn't
+        # installed. (We should get the default value, 'Release').
+        c = self.make_config(exception=OSError)
+        actual = c.default_configuration()
+        self.assertEqual(actual, 'Release')
+
+    def test_default_configuration__scripterror(self):
+        # We run webkit-build-directory to find out where the default
+        # configuration file is. See what happens if that script fails.
+        # (We should get the default value, 'Release').
+        c = self.make_config(exception=ScriptError())
+        actual = c.default_configuration()
+        self.assertEqual(actual, 'Release')
+
+    def test_path_from_webkit_base(self):
+        c = config.Config(MockExecutive(), MockFileSystem())
+        self.assertTrue(c.path_from_webkit_base('foo'))
+
+    def test_webkit_base_dir(self):
+        # FIXME: We use a real filesystem here. Should this move to a mocked one?
+        executive = Executive()
+        filesystem = FileSystem()
+        c = config.Config(executive, filesystem)
+        base_dir = c.webkit_base_dir()
+        self.assertTrue(base_dir)
+        self.assertNotEqual(base_dir[-1], '/')
+
+        # FIXME: Once we use a MockFileSystem for this test we don't need to save the orig_cwd.
+        orig_cwd = filesystem.getcwd()
+        if sys.platform == 'win32':
+            filesystem.chdir(os.environ['USERPROFILE'])
+        else:
+            filesystem.chdir(os.environ['HOME'])
+        c = config.Config(executive, filesystem)
+        try:
+            base_dir_2 = c.webkit_base_dir()
+            self.assertEqual(base_dir, base_dir_2)
+        finally:
+            filesystem.chdir(orig_cwd)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/driver.py b/Tools/Scripts/webkitpy/layout_tests/port/driver.py
new file mode 100644
index 0000000..7993d05
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/driver.py
@@ -0,0 +1,539 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import base64
+import copy
+import logging
+import re
+import shlex
+import sys
+import time
+import os
+
+from webkitpy.common.system import path
+
+
+_log = logging.getLogger(__name__)
+
+
+class DriverInput(object):
+    def __init__(self, test_name, timeout, image_hash, should_run_pixel_test, args=None):
+        self.test_name = test_name
+        self.timeout = timeout  # in ms
+        self.image_hash = image_hash
+        self.should_run_pixel_test = should_run_pixel_test
+        self.args = args or []
+
+
+class DriverOutput(object):
+    """Groups information about a output from driver for easy passing
+    and post-processing of data."""
+
+    strip_patterns = []
+    strip_patterns.append((re.compile('at \(-?[0-9]+,-?[0-9]+\) *'), ''))
+    strip_patterns.append((re.compile('size -?[0-9]+x-?[0-9]+ *'), ''))
+    strip_patterns.append((re.compile('text run width -?[0-9]+: '), ''))
+    strip_patterns.append((re.compile('text run width -?[0-9]+ [a-zA-Z ]+: '), ''))
+    strip_patterns.append((re.compile('RenderButton {BUTTON} .*'), 'RenderButton {BUTTON}'))
+    strip_patterns.append((re.compile('RenderImage {INPUT} .*'), 'RenderImage {INPUT}'))
+    strip_patterns.append((re.compile('RenderBlock {INPUT} .*'), 'RenderBlock {INPUT}'))
+    strip_patterns.append((re.compile('RenderTextControl {INPUT} .*'), 'RenderTextControl {INPUT}'))
+    strip_patterns.append((re.compile('\([0-9]+px'), 'px'))
+    strip_patterns.append((re.compile(' *" *\n +" *'), ' '))
+    strip_patterns.append((re.compile('" +$'), '"'))
+    strip_patterns.append((re.compile('- '), '-'))
+    strip_patterns.append((re.compile('\n( *)"\s+'), '\n\g<1>"'))
+    strip_patterns.append((re.compile('\s+"\n'), '"\n'))
+    strip_patterns.append((re.compile('scrollWidth [0-9]+'), 'scrollWidth'))
+    strip_patterns.append((re.compile('scrollHeight [0-9]+'), 'scrollHeight'))
+    strip_patterns.append((re.compile('scrollX [0-9]+'), 'scrollX'))
+    strip_patterns.append((re.compile('scrollY [0-9]+'), 'scrollY'))
+    strip_patterns.append((re.compile('scrolled to [0-9]+,[0-9]+'), 'scrolled'))
+
+    def __init__(self, text, image, image_hash, audio, crash=False,
+            test_time=0, measurements=None, timeout=False, error='', crashed_process_name='??',
+            crashed_pid=None, crash_log=None):
+        # FIXME: Args could be renamed to better clarify what they do.
+        self.text = text
+        self.image = image  # May be empty-string if the test crashes.
+        self.image_hash = image_hash
+        self.image_diff = None  # image_diff gets filled in after construction.
+        self.audio = audio  # Binary format is port-dependent.
+        self.crash = crash
+        self.crashed_process_name = crashed_process_name
+        self.crashed_pid = crashed_pid
+        self.crash_log = crash_log
+        self.test_time = test_time
+        self.measurements = measurements
+        self.timeout = timeout
+        self.error = error  # stderr output
+
+    def has_stderr(self):
+        return bool(self.error)
+
+    def strip_metrics(self):
+        if not self.text:
+            return
+        for pattern in self.strip_patterns:
+            self.text = re.sub(pattern[0], pattern[1], self.text)
+
+
+class Driver(object):
+    """object for running test(s) using DumpRenderTree/WebKitTestRunner."""
+
+    def __init__(self, port, worker_number, pixel_tests, no_timeout=False):
+        """Initialize a Driver to subsequently run tests.
+
+        Typically this routine will spawn DumpRenderTree in a config
+        ready for subsequent input.
+
+        port - reference back to the port object.
+        worker_number - identifier for a particular worker/driver instance
+        """
+        self._port = port
+        self._worker_number = worker_number
+        self._no_timeout = no_timeout
+
+        self._driver_tempdir = None
+        # WebKitTestRunner can report back subprocess crashes by printing
+        # "#CRASHED - PROCESSNAME".  Since those can happen at any time
+        # and ServerProcess won't be aware of them (since the actual tool
+        # didn't crash, just a subprocess) we record the crashed subprocess name here.
+        self._crashed_process_name = None
+        self._crashed_pid = None
+
+        # WebKitTestRunner can report back subprocesses that became unresponsive
+        # This could mean they crashed.
+        self._subprocess_was_unresponsive = False
+
+        # stderr reading is scoped on a per-test (not per-block) basis, so we store the accumulated
+        # stderr output, as well as if we've seen #EOF on this driver instance.
+        # FIXME: We should probably remove _read_first_block and _read_optional_image_block and
+        # instead scope these locally in run_test.
+        self.error_from_test = str()
+        self.err_seen_eof = False
+        self._server_process = None
+
+        self._measurements = {}
+
+    def __del__(self):
+        self.stop()
+
+    def run_test(self, driver_input, stop_when_done):
+        """Run a single test and return the results.
+
+        Note that it is okay if a test times out or crashes and leaves
+        the driver in an indeterminate state. The upper layers of the program
+        are responsible for cleaning up and ensuring things are okay.
+
+        Returns a DriverOuput object.
+        """
+        start_time = time.time()
+        self.start(driver_input.should_run_pixel_test, driver_input.args)
+        test_begin_time = time.time()
+        self.error_from_test = str()
+        self.err_seen_eof = False
+
+        command = self._command_from_driver_input(driver_input)
+        deadline = test_begin_time + int(driver_input.timeout) / 1000.0
+
+        self._server_process.write(command)
+        text, audio = self._read_first_block(deadline)  # First block is either text or audio
+        image, actual_image_hash = self._read_optional_image_block(deadline)  # The second (optional) block is image data.
+
+        crashed = self.has_crashed()
+        timed_out = self._server_process.timed_out
+
+        if stop_when_done or crashed or timed_out:
+            # We call stop() even if we crashed or timed out in order to get any remaining stdout/stderr output.
+            # In the timeout case, we kill the hung process as well.
+            out, err = self._server_process.stop(self._port.driver_stop_timeout() if stop_when_done else 0.0)
+            if out:
+                text += out
+            if err:
+                self.error_from_test += err
+            self._server_process = None
+
+        crash_log = None
+        if crashed:
+            self.error_from_test, crash_log = self._get_crash_log(text, self.error_from_test, newer_than=start_time)
+
+            # If we don't find a crash log use a placeholder error message instead.
+            if not crash_log:
+                pid_str = str(self._crashed_pid) if self._crashed_pid else "unknown pid"
+                crash_log = 'No crash log found for %s:%s.\n' % (self._crashed_process_name, pid_str)
+                # If we were unresponsive append a message informing there may not have been a crash.
+                if self._subprocess_was_unresponsive:
+                    crash_log += 'Process failed to become responsive before timing out.\n'
+
+                # Print stdout and stderr to the placeholder crash log; we want as much context as possible.
+                if self.error_from_test:
+                    crash_log += '\nstdout:\n%s\nstderr:\n%s\n' % (text, self.error_from_test)
+
+        return DriverOutput(text, image, actual_image_hash, audio,
+            crash=crashed, test_time=time.time() - test_begin_time, measurements=self._measurements,
+            timeout=timed_out, error=self.error_from_test,
+            crashed_process_name=self._crashed_process_name,
+            crashed_pid=self._crashed_pid, crash_log=crash_log)
+
+    def _get_crash_log(self, stdout, stderr, newer_than):
+        return self._port._get_crash_log(self._crashed_process_name, self._crashed_pid, stdout, stderr, newer_than)
+
+    # FIXME: Seems this could just be inlined into callers.
+    @classmethod
+    def _command_wrapper(cls, wrapper_option):
+        # Hook for injecting valgrind or other runtime instrumentation,
+        # used by e.g. tools/valgrind/valgrind_tests.py.
+        return shlex.split(wrapper_option) if wrapper_option else []
+
+    HTTP_DIR = "http/tests/"
+    HTTP_LOCAL_DIR = "http/tests/local/"
+
+    def is_http_test(self, test_name):
+        return test_name.startswith(self.HTTP_DIR) and not test_name.startswith(self.HTTP_LOCAL_DIR)
+
+    def test_to_uri(self, test_name):
+        """Convert a test name to a URI."""
+        if not self.is_http_test(test_name):
+            return path.abspath_to_uri(self._port.host.platform, self._port.abspath_for_test(test_name))
+
+        relative_path = test_name[len(self.HTTP_DIR):]
+
+        # TODO(dpranke): remove the SSL reference?
+        if relative_path.startswith("ssl/"):
+            return "https://127.0.0.1:8443/" + relative_path
+        return "http://127.0.0.1:8000/" + relative_path
+
+    def uri_to_test(self, uri):
+        """Return the base layout test name for a given URI.
+
+        This returns the test name for a given URI, e.g., if you passed in
+        "file:///src/LayoutTests/fast/html/keygen.html" it would return
+        "fast/html/keygen.html".
+
+        """
+        if uri.startswith("file:///"):
+            prefix = path.abspath_to_uri(self._port.host.platform, self._port.layout_tests_dir())
+            if not prefix.endswith('/'):
+                prefix += '/'
+            return uri[len(prefix):]
+        if uri.startswith("http://"):
+            return uri.replace('http://127.0.0.1:8000/', self.HTTP_DIR)
+        if uri.startswith("https://"):
+            return uri.replace('https://127.0.0.1:8443/', self.HTTP_DIR)
+        raise NotImplementedError('unknown url type: %s' % uri)
+
+    def has_crashed(self):
+        if self._server_process is None:
+            return False
+        if self._crashed_process_name:
+            return True
+        if self._server_process.has_crashed():
+            self._crashed_process_name = self._server_process.name()
+            self._crashed_pid = self._server_process.pid()
+            return True
+        return False
+
+    def start(self, pixel_tests, per_test_args):
+        # FIXME: Callers shouldn't normally call this, since this routine
+        # may not be specifying the correct combination of pixel test and
+        # per_test args.
+        #
+        # The only reason we have this routine at all is so the perftestrunner
+        # can pause before running a test; it might be better to push that
+        # into run_test() directly.
+        if not self._server_process:
+            self._start(pixel_tests, per_test_args)
+
+    def _start(self, pixel_tests, per_test_args):
+        self.stop()
+        self._driver_tempdir = self._port._filesystem.mkdtemp(prefix='%s-' % self._port.driver_name())
+        server_name = self._port.driver_name()
+        environment = self._port.setup_environ_for_server(server_name)
+        environment['DYLD_LIBRARY_PATH'] = self._port._build_path()
+        environment['DYLD_FRAMEWORK_PATH'] = self._port._build_path()
+        # FIXME: We're assuming that WebKitTestRunner checks this DumpRenderTree-named environment variable.
+        environment['DUMPRENDERTREE_TEMP'] = str(self._driver_tempdir)
+        environment['LOCAL_RESOURCE_ROOT'] = self._port.layout_tests_dir()
+        if 'WEBKITOUTPUTDIR' in os.environ:
+            environment['WEBKITOUTPUTDIR'] = os.environ['WEBKITOUTPUTDIR']
+        self._crashed_process_name = None
+        self._crashed_pid = None
+        self._server_process = self._port._server_process_constructor(self._port, server_name, self.cmd_line(pixel_tests, per_test_args), environment)
+        self._server_process.start()
+
+    def stop(self):
+        if self._server_process:
+            self._server_process.stop(self._port.driver_stop_timeout())
+            self._server_process = None
+
+        if self._driver_tempdir:
+            self._port._filesystem.rmtree(str(self._driver_tempdir))
+            self._driver_tempdir = None
+
+    def cmd_line(self, pixel_tests, per_test_args):
+        cmd = self._command_wrapper(self._port.get_option('wrapper'))
+        cmd.append(self._port._path_to_driver())
+        if self._port.get_option('gc_between_tests'):
+            cmd.append('--gc-between-tests')
+        if self._port.get_option('complex_text'):
+            cmd.append('--complex-text')
+        if self._port.get_option('threaded'):
+            cmd.append('--threaded')
+        if self._no_timeout:
+            cmd.append('--no-timeout')
+        # FIXME: We need to pass --timeout=SECONDS to WebKitTestRunner for WebKit2.
+
+        cmd.extend(self._port.get_option('additional_drt_flag', []))
+        cmd.extend(self._port.additional_drt_flag())
+
+        cmd.extend(per_test_args)
+
+        cmd.append('-')
+        return cmd
+
+    def _check_for_driver_crash(self, error_line):
+        if error_line == "#CRASHED\n":
+            # This is used on Windows to report that the process has crashed
+            # See http://trac.webkit.org/changeset/65537.
+            self._crashed_process_name = self._server_process.name()
+            self._crashed_pid = self._server_process.pid()
+        elif (error_line.startswith("#CRASHED - ")
+            or error_line.startswith("#PROCESS UNRESPONSIVE - ")):
+            # WebKitTestRunner uses this to report that the WebProcess subprocess crashed.
+            match = re.match('#(?:CRASHED|PROCESS UNRESPONSIVE) - (\S+)', error_line)
+            self._crashed_process_name = match.group(1) if match else 'WebProcess'
+            match = re.search('pid (\d+)', error_line)
+            pid = int(match.group(1)) if match else None
+            self._crashed_pid = pid
+            # FIXME: delete this after we're sure this code is working :)
+            _log.debug('%s crash, pid = %s, error_line = %s' % (self._crashed_process_name, str(pid), error_line))
+            if error_line.startswith("#PROCESS UNRESPONSIVE - "):
+                self._subprocess_was_unresponsive = True
+                # We want to show this since it's not a regular crash and probably we don't have a crash log.
+                self.error_from_test += error_line
+            return True
+        return self.has_crashed()
+
+    def _command_from_driver_input(self, driver_input):
+        # FIXME: performance tests pass in full URLs instead of test names.
+        if driver_input.test_name.startswith('http://') or driver_input.test_name.startswith('https://')  or driver_input.test_name == ('about:blank'):
+            command = driver_input.test_name
+        elif self.is_http_test(driver_input.test_name):
+            command = self.test_to_uri(driver_input.test_name)
+        else:
+            command = self._port.abspath_for_test(driver_input.test_name)
+            if sys.platform == 'cygwin':
+                command = path.cygpath(command)
+
+        assert not driver_input.image_hash or driver_input.should_run_pixel_test
+
+        # ' is the separator between arguments.
+        if driver_input.should_run_pixel_test:
+            command += "'--pixel-test"
+        if driver_input.image_hash:
+            command += "'" + driver_input.image_hash
+        return command + "\n"
+
+    def _read_first_block(self, deadline):
+        # returns (text_content, audio_content)
+        block = self._read_block(deadline)
+        if block.malloc:
+            self._measurements['Malloc'] = float(block.malloc)
+        if block.js_heap:
+            self._measurements['JSHeap'] = float(block.js_heap)
+        if block.content_type == 'audio/wav':
+            return (None, block.decoded_content)
+        return (block.decoded_content, None)
+
+    def _read_optional_image_block(self, deadline):
+        # returns (image, actual_image_hash)
+        block = self._read_block(deadline, wait_for_stderr_eof=True)
+        if block.content and block.content_type == 'image/png':
+            return (block.decoded_content, block.content_hash)
+        return (None, block.content_hash)
+
+    def _read_header(self, block, line, header_text, header_attr, header_filter=None):
+        if line.startswith(header_text) and getattr(block, header_attr) is None:
+            value = line.split()[1]
+            if header_filter:
+                value = header_filter(value)
+            setattr(block, header_attr, value)
+            return True
+        return False
+
+    def _process_stdout_line(self, block, line):
+        if (self._read_header(block, line, 'Content-Type: ', 'content_type')
+            or self._read_header(block, line, 'Content-Transfer-Encoding: ', 'encoding')
+            or self._read_header(block, line, 'Content-Length: ', '_content_length', int)
+            or self._read_header(block, line, 'ActualHash: ', 'content_hash')
+            or self._read_header(block, line, 'DumpMalloc: ', 'malloc')
+            or self._read_header(block, line, 'DumpJSHeap: ', 'js_heap')):
+            return
+        # Note, we're not reading ExpectedHash: here, but we could.
+        # If the line wasn't a header, we just append it to the content.
+        block.content += line
+
+    def _strip_eof(self, line):
+        if line and line.endswith("#EOF\n"):
+            return line[:-5], True
+        return line, False
+
+    def _read_block(self, deadline, wait_for_stderr_eof=False):
+        block = ContentBlock()
+        out_seen_eof = False
+
+        while not self.has_crashed():
+            if out_seen_eof and (self.err_seen_eof or not wait_for_stderr_eof):
+                break
+
+            if self.err_seen_eof:
+                out_line = self._server_process.read_stdout_line(deadline)
+                err_line = None
+            elif out_seen_eof:
+                out_line = None
+                err_line = self._server_process.read_stderr_line(deadline)
+            else:
+                out_line, err_line = self._server_process.read_either_stdout_or_stderr_line(deadline)
+
+            if self._server_process.timed_out or self.has_crashed():
+                break
+
+            if out_line:
+                assert not out_seen_eof
+                out_line, out_seen_eof = self._strip_eof(out_line)
+            if err_line:
+                assert not self.err_seen_eof
+                err_line, self.err_seen_eof = self._strip_eof(err_line)
+
+            if out_line:
+                if out_line[-1] != "\n":
+                    _log.error("Last character read from DRT stdout line was not a newline!  This indicates either a NRWT or DRT bug.")
+                content_length_before_header_check = block._content_length
+                self._process_stdout_line(block, out_line)
+                # FIXME: Unlike HTTP, DRT dumps the content right after printing a Content-Length header.
+                # Don't wait until we're done with headers, just read the binary blob right now.
+                if content_length_before_header_check != block._content_length:
+                    block.content = self._server_process.read_stdout(deadline, block._content_length)
+
+            if err_line:
+                if self._check_for_driver_crash(err_line):
+                    break
+                self.error_from_test += err_line
+
+        block.decode_content()
+        return block
+
+
+class ContentBlock(object):
+    def __init__(self):
+        self.content_type = None
+        self.encoding = None
+        self.content_hash = None
+        self._content_length = None
+        # Content is treated as binary data even though the text output is usually UTF-8.
+        self.content = str()  # FIXME: Should be bytearray() once we require Python 2.6.
+        self.decoded_content = None
+        self.malloc = None
+        self.js_heap = None
+
+    def decode_content(self):
+        if self.encoding == 'base64' and self.content is not None:
+            self.decoded_content = base64.b64decode(self.content)
+        else:
+            self.decoded_content = self.content
+
+class DriverProxy(object):
+    """A wrapper for managing two Driver instances, one with pixel tests and
+    one without. This allows us to handle plain text tests and ref tests with a
+    single driver."""
+
+    def __init__(self, port, worker_number, driver_instance_constructor, pixel_tests, no_timeout):
+        self._port = port
+        self._worker_number = worker_number
+        self._driver_instance_constructor = driver_instance_constructor
+        self._no_timeout = no_timeout
+
+        # FIXME: We shouldn't need to create a driver until we actually run a test.
+        self._driver = self._make_driver(pixel_tests)
+        self._running_drivers = {}
+        self._running_drivers[self._cmd_line_as_key(pixel_tests, [])] = self._driver
+
+    def _make_driver(self, pixel_tests):
+        return self._driver_instance_constructor(self._port, self._worker_number, pixel_tests, self._no_timeout)
+
+    # FIXME: this should be a @classmethod (or implemented on Port instead).
+    def is_http_test(self, test_name):
+        return self._driver.is_http_test(test_name)
+
+    # FIXME: this should be a @classmethod (or implemented on Port instead).
+    def test_to_uri(self, test_name):
+        return self._driver.test_to_uri(test_name)
+
+    # FIXME: this should be a @classmethod (or implemented on Port instead).
+    def uri_to_test(self, uri):
+        return self._driver.uri_to_test(uri)
+
+    def run_test(self, driver_input, stop_when_done):
+        base = self._port.lookup_virtual_test_base(driver_input.test_name)
+        if base:
+            virtual_driver_input = copy.copy(driver_input)
+            virtual_driver_input.test_name = base
+            virtual_driver_input.args = self._port.lookup_virtual_test_args(driver_input.test_name)
+            return self.run_test(virtual_driver_input, stop_when_done)
+
+        pixel_tests_needed = driver_input.should_run_pixel_test
+        cmd_line_key = self._cmd_line_as_key(pixel_tests_needed, driver_input.args)
+        if not cmd_line_key in self._running_drivers:
+            self._running_drivers[cmd_line_key] = self._make_driver(pixel_tests_needed)
+
+        return self._running_drivers[cmd_line_key].run_test(driver_input, stop_when_done)
+
+    def start(self):
+        # FIXME: Callers shouldn't normally call this, since this routine
+        # may not be specifying the correct combination of pixel test and
+        # per_test args.
+        #
+        # The only reason we have this routine at all is so the perftestrunner
+        # can pause before running a test; it might be better to push that
+        # into run_test() directly.
+        self._driver.start(self._port.get_option('pixel_tests'), [])
+
+    def has_crashed(self):
+        return any(driver.has_crashed() for driver in self._running_drivers.values())
+
+    def stop(self):
+        for driver in self._running_drivers.values():
+            driver.stop()
+
+    # FIXME: this should be a @classmethod (or implemented on Port instead).
+    def cmd_line(self, pixel_tests=None, per_test_args=None):
+        return self._driver.cmd_line(pixel_tests, per_test_args or [])
+
+    def _cmd_line_as_key(self, pixel_tests, per_test_args):
+        return ' '.join(self.cmd_line(pixel_tests, per_test_args))
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/driver_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/driver_unittest.py
new file mode 100644
index 0000000..5b11ade
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/driver_unittest.py
@@ -0,0 +1,268 @@
+# 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.system.systemhost_mock import MockSystemHost
+
+from webkitpy.layout_tests.port import Port, Driver, DriverOutput
+from webkitpy.layout_tests.port.server_process_mock import MockServerProcess
+
+# FIXME: remove the dependency on TestWebKitPort
+from webkitpy.layout_tests.port.port_testcase import TestWebKitPort
+
+
+class DriverOutputTest(unittest.TestCase):
+    def test_strip_metrics(self):
+        patterns = [
+            ('RenderView at (0,0) size 800x600', 'RenderView '),
+            ('text run at (0,0) width 100: "some text"', '"some text"'),
+            ('RenderBlock {HTML} at (0,0) size 800x600', 'RenderBlock {HTML} '),
+            ('RenderBlock {INPUT} at (29,3) size 12x12 [color=#000000]', 'RenderBlock {INPUT}'),
+
+            ('RenderBlock (floating) {DT} at (5,5) size 79x310 [border: (5px solid #000000)]',
+            'RenderBlock (floating) {DT} [border: px solid #000000)]'),
+
+            ('\n    "truncate text    "\n', '\n    "truncate text"\n'),
+
+            ('RenderText {#text} at (0,3) size 41x12\n    text run at (0,3) width 41: "whimper "\n',
+            'RenderText {#text} \n    "whimper"\n'),
+
+            ("""text run at (0,0) width 109: ".one {color: green;}"
+          text run at (109,0) width 0: " "
+          text run at (0,17) width 81: ".1 {color: red;}"
+          text run at (81,17) width 0: " "
+          text run at (0,34) width 102: ".a1 {color: green;}"
+          text run at (102,34) width 0: " "
+          text run at (0,51) width 120: "P.two {color: purple;}"
+          text run at (120,51) width 0: " "\n""",
+            '".one {color: green;}  .1 {color: red;}  .a1 {color: green;}  P.two {color: purple;}"\n'),
+
+            ('text-- other text', 'text--other text'),
+
+            (' some output   "truncate trailing spaces at end of line after text"   \n',
+            ' some output   "truncate trailing spaces at end of line after text"\n'),
+
+            (r'scrollWidth 120', r'scrollWidth'),
+            (r'scrollHeight 120', r'scrollHeight'),
+        ]
+
+        for pattern in patterns:
+            driver_output = DriverOutput(pattern[0], None, None, None)
+            driver_output.strip_metrics()
+            self.assertEqual(driver_output.text, pattern[1])
+
+
+class DriverTest(unittest.TestCase):
+    def make_port(self):
+        return Port(MockSystemHost())
+
+    def _assert_wrapper(self, wrapper_string, expected_wrapper):
+        wrapper = Driver(self.make_port(), None, pixel_tests=False)._command_wrapper(wrapper_string)
+        self.assertEqual(wrapper, expected_wrapper)
+
+    def test_command_wrapper(self):
+        self._assert_wrapper(None, [])
+        self._assert_wrapper("valgrind", ["valgrind"])
+
+        # Validate that shlex works as expected.
+        command_with_spaces = "valgrind --smc-check=\"check with spaces!\" --foo"
+        expected_parse = ["valgrind", "--smc-check=check with spaces!", "--foo"]
+        self._assert_wrapper(command_with_spaces, expected_parse)
+
+    def test_test_to_uri(self):
+        port = self.make_port()
+        driver = Driver(port, None, pixel_tests=False)
+        self.assertEqual(driver.test_to_uri('foo/bar.html'), 'file://%s/foo/bar.html' % port.layout_tests_dir())
+        self.assertEqual(driver.test_to_uri('http/tests/foo.html'), 'http://127.0.0.1:8000/foo.html')
+        self.assertEqual(driver.test_to_uri('http/tests/ssl/bar.html'), 'https://127.0.0.1:8443/ssl/bar.html')
+
+    def test_uri_to_test(self):
+        port = self.make_port()
+        driver = Driver(port, None, pixel_tests=False)
+        self.assertEqual(driver.uri_to_test('file://%s/foo/bar.html' % port.layout_tests_dir()), 'foo/bar.html')
+        self.assertEqual(driver.uri_to_test('http://127.0.0.1:8000/foo.html'), 'http/tests/foo.html')
+        self.assertEqual(driver.uri_to_test('https://127.0.0.1:8443/ssl/bar.html'), 'http/tests/ssl/bar.html')
+
+    def test_read_block(self):
+        port = TestWebKitPort()
+        driver = Driver(port, 0, pixel_tests=False)
+        driver._server_process = MockServerProcess(lines=[
+            'ActualHash: foobar',
+            'Content-Type: my_type',
+            'Content-Transfer-Encoding: none',
+            "#EOF",
+        ])
+        content_block = driver._read_block(0)
+        self.assertEquals(content_block.content_type, 'my_type')
+        self.assertEquals(content_block.encoding, 'none')
+        self.assertEquals(content_block.content_hash, 'foobar')
+        driver._server_process = None
+
+    def test_read_binary_block(self):
+        port = TestWebKitPort()
+        driver = Driver(port, 0, pixel_tests=True)
+        driver._server_process = MockServerProcess(lines=[
+            'ActualHash: actual',
+            'ExpectedHash: expected',
+            'Content-Type: image/png',
+            'Content-Length: 9',
+            "12345678",
+            "#EOF",
+        ])
+        content_block = driver._read_block(0)
+        self.assertEquals(content_block.content_type, 'image/png')
+        self.assertEquals(content_block.content_hash, 'actual')
+        self.assertEquals(content_block.content, '12345678\n')
+        self.assertEquals(content_block.decoded_content, '12345678\n')
+        driver._server_process = None
+
+    def test_read_base64_block(self):
+        port = TestWebKitPort()
+        driver = Driver(port, 0, pixel_tests=True)
+        driver._server_process = MockServerProcess(lines=[
+            'ActualHash: actual',
+            'ExpectedHash: expected',
+            'Content-Type: image/png',
+            'Content-Transfer-Encoding: base64',
+            'Content-Length: 12',
+            'MTIzNDU2NzgK#EOF',
+        ])
+        content_block = driver._read_block(0)
+        self.assertEquals(content_block.content_type, 'image/png')
+        self.assertEquals(content_block.content_hash, 'actual')
+        self.assertEquals(content_block.encoding, 'base64')
+        self.assertEquals(content_block.content, 'MTIzNDU2NzgK')
+        self.assertEquals(content_block.decoded_content, '12345678\n')
+
+    def test_no_timeout(self):
+        port = TestWebKitPort()
+        driver = Driver(port, 0, pixel_tests=True, no_timeout=True)
+        self.assertEquals(driver.cmd_line(True, []), ['/mock-build/DumpRenderTree', '--no-timeout', '-'])
+
+    def test_check_for_driver_crash(self):
+        port = TestWebKitPort()
+        driver = Driver(port, 0, pixel_tests=True)
+
+        class FakeServerProcess(object):
+            def __init__(self, crashed):
+                self.crashed = crashed
+
+            def pid(self):
+                return 1234
+
+            def name(self):
+                return 'FakeServerProcess'
+
+            def has_crashed(self):
+                return self.crashed
+
+            def stop(self, timeout):
+                pass
+
+        def assert_crash(driver, error_line, crashed, name, pid, unresponsive=False):
+            self.assertEquals(driver._check_for_driver_crash(error_line), crashed)
+            self.assertEquals(driver._crashed_process_name, name)
+            self.assertEquals(driver._crashed_pid, pid)
+            self.assertEquals(driver._subprocess_was_unresponsive, unresponsive)
+            driver.stop()
+
+        driver._server_process = FakeServerProcess(False)
+        assert_crash(driver, '', False, None, None)
+
+        driver._crashed_process_name = None
+        driver._crashed_pid = None
+        driver._server_process = FakeServerProcess(False)
+        driver._subprocess_was_unresponsive = False
+        assert_crash(driver, '#CRASHED\n', True, 'FakeServerProcess', 1234)
+
+        driver._crashed_process_name = None
+        driver._crashed_pid = None
+        driver._server_process = FakeServerProcess(False)
+        driver._subprocess_was_unresponsive = False
+        assert_crash(driver, '#CRASHED - WebProcess\n', True, 'WebProcess', None)
+
+        driver._crashed_process_name = None
+        driver._crashed_pid = None
+        driver._server_process = FakeServerProcess(False)
+        driver._subprocess_was_unresponsive = False
+        assert_crash(driver, '#CRASHED - WebProcess (pid 8675)\n', True, 'WebProcess', 8675)
+
+        driver._crashed_process_name = None
+        driver._crashed_pid = None
+        driver._server_process = FakeServerProcess(False)
+        driver._subprocess_was_unresponsive = False
+        assert_crash(driver, '#PROCESS UNRESPONSIVE - WebProcess (pid 8675)\n', True, 'WebProcess', 8675, True)
+
+        driver._crashed_process_name = None
+        driver._crashed_pid = None
+        driver._server_process = FakeServerProcess(False)
+        driver._subprocess_was_unresponsive = False
+        assert_crash(driver, '#CRASHED - renderer (pid 8675)\n', True, 'renderer', 8675)
+
+        driver._crashed_process_name = None
+        driver._crashed_pid = None
+        driver._server_process = FakeServerProcess(True)
+        driver._subprocess_was_unresponsive = False
+        assert_crash(driver, '', True, 'FakeServerProcess', 1234)
+
+    def test_creating_a_port_does_not_write_to_the_filesystem(self):
+        port = TestWebKitPort()
+        driver = Driver(port, 0, pixel_tests=True)
+        self.assertEquals(port._filesystem.written_files, {})
+        self.assertEquals(port._filesystem.last_tmpdir, None)
+
+    def test_stop_cleans_up_properly(self):
+        port = TestWebKitPort()
+        port._server_process_constructor = MockServerProcess
+        driver = Driver(port, 0, pixel_tests=True)
+        driver.start(True, [])
+        last_tmpdir = port._filesystem.last_tmpdir
+        self.assertNotEquals(last_tmpdir, None)
+        driver.stop()
+        self.assertFalse(port._filesystem.isdir(last_tmpdir))
+
+    def test_two_starts_cleans_up_properly(self):
+        port = TestWebKitPort()
+        port._server_process_constructor = MockServerProcess
+        driver = Driver(port, 0, pixel_tests=True)
+        driver.start(True, [])
+        last_tmpdir = port._filesystem.last_tmpdir
+        driver._start(True, [])
+        self.assertFalse(port._filesystem.isdir(last_tmpdir))
+
+    def test_start_actually_starts(self):
+        port = TestWebKitPort()
+        port._server_process_constructor = MockServerProcess
+        driver = Driver(port, 0, pixel_tests=True)
+        driver.start(True, [])
+        self.assertTrue(driver._server_process.started)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/efl.py b/Tools/Scripts/webkitpy/layout_tests/port/efl.py
new file mode 100644
index 0000000..0c9acd8
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/efl.py
@@ -0,0 +1,123 @@
+# Copyright (C) 2011 ProFUSION Embedded Systems. All rights reserved.
+# Copyright (C) 2011 Samsung Electronics. All rights reserved.
+# Copyright (C) 2012 Intel Corporation
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""WebKit Efl implementation of the Port interface."""
+
+import os
+
+from webkitpy.layout_tests.models.test_configuration import TestConfiguration
+from webkitpy.layout_tests.port.base import Port
+from webkitpy.layout_tests.port.pulseaudio_sanitizer import PulseAudioSanitizer
+from webkitpy.layout_tests.port.xvfbdriver import XvfbDriver
+
+class EflPort(Port, PulseAudioSanitizer):
+    port_name = 'efl'
+
+    def __init__(self, *args, **kwargs):
+        super(EflPort, self).__init__(*args, **kwargs)
+
+        self._jhbuild_wrapper_path = self.path_from_webkit_base('Tools', 'efl', 'run-with-jhbuild')
+
+        self.set_option_default('wrapper', self._jhbuild_wrapper_path)
+        self.webprocess_cmd_prefix = self.get_option('webprocess_cmd_prefix')
+
+    def _port_flag_for_scripts(self):
+        return "--efl"
+
+    def setup_test_run(self):
+        self._unload_pulseaudio_module()
+
+    def setup_environ_for_server(self, server_name=None):
+        env = super(EflPort, self).setup_environ_for_server(server_name)
+        # If DISPLAY environment variable is unset in the system
+        # e.g. on build bot, remove DISPLAY variable from the dictionary
+        if not 'DISPLAY' in os.environ:
+            del env['DISPLAY']
+        env['TEST_RUNNER_INJECTED_BUNDLE_FILENAME'] = self._build_path('lib', 'libTestRunnerInjectedBundle.so')
+        env['TEST_RUNNER_PLUGIN_PATH'] = self._build_path('lib')
+        if self.webprocess_cmd_prefix:
+            env['WEB_PROCESS_CMD_PREFIX'] = self.webprocess_cmd_prefix
+
+        return env
+
+    def default_timeout_ms(self):
+        # Tests run considerably slower under gdb
+        # or valgrind.
+        if self.get_option('webprocess_cmd_prefix'):
+            return 350 * 1000
+        return super(EflPort, self).default_timeout_ms()
+
+    def clean_up_test_run(self):
+        super(EflPort, self).clean_up_test_run()
+        self._restore_pulseaudio_module()
+
+    def _generate_all_test_configurations(self):
+        return [TestConfiguration(version=self._version, architecture='x86', build_type=build_type) for build_type in self.ALL_BUILD_TYPES]
+
+    def _driver_class(self):
+        return XvfbDriver
+
+    def _path_to_driver(self):
+        return self._build_path('bin', self.driver_name())
+
+    def _path_to_image_diff(self):
+        return self._build_path('bin', 'ImageDiff')
+
+    def _image_diff_command(self, *args, **kwargs):
+        return [self._jhbuild_wrapper_path] + super(EflPort, self)._image_diff_command(*args, **kwargs)
+
+    def _path_to_webcore_library(self):
+        static_path = self._build_path('lib', 'libwebcore_efl.a')
+        dyn_path = self._build_path('lib', 'libwebcore_efl.so')
+        return static_path if self._filesystem.exists(static_path) else dyn_path
+
+    def _search_paths(self):
+        search_paths = []
+        if self.get_option('webkit_test_runner'):
+            search_paths.append(self.port_name + '-wk2')
+            search_paths.append('wk2')
+        else:
+            search_paths.append(self.port_name + '-wk1')
+        search_paths.append(self.port_name)
+        return search_paths
+
+    def default_baseline_search_path(self):
+        return map(self._webkit_baseline_path, self._search_paths())
+
+    def expectations_files(self):
+        # FIXME: We should be able to use the default algorithm here.
+        return list(reversed([self._filesystem.join(self._webkit_baseline_path(p), 'TestExpectations') for p in self._search_paths()]))
+
+    def show_results_html_file(self, results_filename):
+        # FIXME: We should find a way to share this implmentation with Gtk,
+        # or teach run-launcher how to call run-safari and move this down to WebKitPort.
+        run_launcher_args = ["file://%s" % results_filename]
+        if self.get_option('webkit_test_runner'):
+            run_launcher_args.append('-2')
+        # FIXME: old-run-webkit-tests also added ["-graphicssystem", "raster", "-style", "windows"]
+        # FIXME: old-run-webkit-tests converted results_filename path for cygwin.
+        self._run_script("run-launcher", run_launcher_args)
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/efl_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/efl_unittest.py
new file mode 100644
index 0000000..d9851b3
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/efl_unittest.py
@@ -0,0 +1,43 @@
+# Copyright (C) 2011 ProFUSION Embedded Systems. All rights reserved.
+# Copyright (C) 2011 Samsung Electronics. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.layout_tests.port.efl import EflPort
+from webkitpy.layout_tests.port import port_testcase
+from webkitpy.common.system.executive_mock import MockExecutive
+
+
+class EflPortTest(port_testcase.PortTestCase):
+    port_name = 'efl'
+    port_maker = EflPort
+
+    def test_show_results_html_file(self):
+        port = self.make_port()
+        port._executive = MockExecutive(should_log=True)
+        expected_stderr = "MOCK run_command: ['Tools/Scripts/run-launcher', '--release', '--efl', 'file://test.html'], cwd=/mock-checkout\n"
+        OutputCapture().assert_outputs(self, port.show_results_html_file, ["test.html"], expected_stderr=expected_stderr)
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/factory.py b/Tools/Scripts/webkitpy/layout_tests/port/factory.py
new file mode 100644
index 0000000..ad7c644
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/factory.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Factory method to retrieve the appropriate port implementation."""
+
+import fnmatch
+import optparse
+import re
+
+from webkitpy.layout_tests.port import builders
+
+
+def platform_options(use_globs=False):
+    return [
+        optparse.make_option('--platform', action='store',
+            help=('Glob-style list of platform/ports to use (e.g., "mac*")' if use_globs else 'Platform to use (e.g., "mac-lion")')),
+        optparse.make_option('--chromium', action='store_const', dest='platform',
+            const=('chromium*' if use_globs else 'chromium'),
+            help=('Alias for --platform=chromium*' if use_globs else 'Alias for --platform=chromium')),
+        optparse.make_option('--chromium-android', action='store_const', dest='platform',
+            const=('chromium-android*' if use_globs else 'chromium-android'),
+            help=('Alias for --platform=chromium-android*' if use_globs else 'Alias for --platform=chromium')),
+        optparse.make_option('--efl', action='store_const', dest='platform',
+            const=('efl*' if use_globs else 'efl'),
+            help=('Alias for --platform=efl*' if use_globs else 'Alias for --platform=efl')),
+        optparse.make_option('--gtk', action='store_const', dest='platform',
+            const=('gtk*' if use_globs else 'gtk'),
+            help=('Alias for --platform=gtk*' if use_globs else 'Alias for --platform=gtk')),
+        optparse.make_option('--qt', action='store_const', dest="platform",
+            const=('qt*' if use_globs else 'qt'),
+            help=('Alias for --platform=qt' if use_globs else 'Alias for --platform=qt')),
+        ]
+
+
+def configuration_options():
+    return [
+        optparse.make_option("-t", "--target", dest="configuration", help="(DEPRECATED)"),
+        # FIXME: --help should display which configuration is default.
+        optparse.make_option('--debug', action='store_const', const='Debug', dest="configuration",
+            help='Set the configuration to Debug'),
+        optparse.make_option('--release', action='store_const', const='Release', dest="configuration",
+            help='Set the configuration to Release'),
+        optparse.make_option('--32-bit', action='store_const', const='x86', default=None, dest="architecture",
+            help='use 32-bit binaries by default (x86 instead of x86_64)'),
+        ]
+
+
+
+def _builder_options(builder_name):
+    configuration = "Debug" if re.search(r"[d|D](ebu|b)g", builder_name) else "Release"
+    is_webkit2 = builder_name.find("WK2") != -1
+    builder_name = builder_name
+    return optparse.Values({'builder_name': builder_name, 'configuration': configuration, 'webkit_test_runner': is_webkit2})
+
+
+class PortFactory(object):
+    PORT_CLASSES = (
+        'chromium_android.ChromiumAndroidPort',
+        'chromium_linux.ChromiumLinuxPort',
+        'chromium_mac.ChromiumMacPort',
+        'chromium_win.ChromiumWinPort',
+        'efl.EflPort',
+        'gtk.GtkPort',
+        'mac.MacPort',
+        'mock_drt.MockDRTPort',
+        'qt.QtPort',
+        'test.TestPort',
+        'win.WinPort',
+    )
+
+    def __init__(self, host):
+        self._host = host
+
+    def _default_port(self, options):
+        platform = self._host.platform
+        if platform.is_linux() or platform.is_freebsd():
+            return 'chromium-linux'
+        elif platform.is_mac():
+            return 'mac'
+        elif platform.is_win():
+            return 'win'
+        raise NotImplementedError('unknown platform: %s' % platform)
+
+    def get(self, port_name=None, options=None, **kwargs):
+        """Returns an object implementing the Port interface. If
+        port_name is None, this routine attempts to guess at the most
+        appropriate port on this platform."""
+        port_name = port_name or self._default_port(options)
+
+        # FIXME(dpranke): We special-case '--platform chromium' so that it can co-exist
+        # with '--platform chromium-mac' and '--platform chromium-linux' properly (we
+        # can't look at the port_name prefix in this case).
+        if port_name == 'chromium':
+            port_name = 'chromium-' + self._host.platform.os_name
+
+        for port_class in self.PORT_CLASSES:
+            module_name, class_name = port_class.rsplit('.', 1)
+            module = __import__(module_name, globals(), locals(), [], -1)
+            cls = module.__dict__[class_name]
+            if port_name.startswith(cls.port_name):
+                port_name = cls.determine_full_port_name(self._host, options, port_name)
+                return cls(self._host, port_name, options=options, **kwargs)
+        raise NotImplementedError('unsupported platform: "%s"' % port_name)
+
+    def all_port_names(self, platform=None):
+        """Return a list of all valid, fully-specified, "real" port names.
+
+        This is the list of directories that are used as actual baseline_paths()
+        by real ports. This does not include any "fake" names like "test"
+        or "mock-mac", and it does not include any directories that are not.
+
+        If platform is not specified, we will glob-match all ports"""
+        platform = platform or '*'
+        return fnmatch.filter(builders.all_port_names(), platform)
+
+    def get_from_builder_name(self, builder_name):
+        port_name = builders.port_name_for_builder_name(builder_name)
+        assert port_name, "unrecognized builder name '%s'" % builder_name
+        return self.get(port_name, _builder_options(builder_name))
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/factory_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/factory_unittest.py
new file mode 100644
index 0000000..2980c2d
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/factory_unittest.py
@@ -0,0 +1,113 @@
+# 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.tool.mocktool import MockOptions
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+
+from webkitpy.layout_tests.port import chromium_android
+from webkitpy.layout_tests.port import chromium_linux
+from webkitpy.layout_tests.port import chromium_mac
+from webkitpy.layout_tests.port import chromium_win
+from webkitpy.layout_tests.port import factory
+from webkitpy.layout_tests.port import gtk
+from webkitpy.layout_tests.port import mac
+from webkitpy.layout_tests.port import qt
+from webkitpy.layout_tests.port import test
+from webkitpy.layout_tests.port import win
+
+
+class FactoryTest(unittest.TestCase):
+    """Test that the factory creates the proper port object for given combination of port_name, host.platform, and options."""
+    # FIXME: The ports themselves should expose what options they require,
+    # instead of passing generic "options".
+
+    def setUp(self):
+        self.webkit_options = MockOptions(pixel_tests=False)
+
+    def assert_port(self, port_name=None, os_name=None, os_version=None, options=None, cls=None):
+        host = MockSystemHost(os_name=os_name, os_version=os_version)
+        port = factory.PortFactory(host).get(port_name, options=options)
+        self.assertTrue(isinstance(port, cls))
+
+    def test_mac(self):
+        self.assert_port(port_name='mac-lion', cls=mac.MacPort)
+        self.assert_port(port_name='mac-lion-wk2', cls=mac.MacPort)
+        self.assert_port(port_name='mac', os_name='mac', os_version='lion', cls=mac.MacPort)
+        self.assert_port(port_name=None,  os_name='mac', os_version='lion', cls=mac.MacPort)
+
+    def test_win(self):
+        self.assert_port(port_name='win-xp', cls=win.WinPort)
+        self.assert_port(port_name='win-xp-wk2', cls=win.WinPort)
+        self.assert_port(port_name='win', os_name='win', os_version='xp', cls=win.WinPort)
+        self.assert_port(port_name=None, os_name='win', os_version='xp', cls=win.WinPort)
+        self.assert_port(port_name=None, os_name='win', os_version='xp', options=self.webkit_options, cls=win.WinPort)
+
+    def test_gtk(self):
+        self.assert_port(port_name='gtk', cls=gtk.GtkPort)
+
+    def test_qt(self):
+        self.assert_port(port_name='qt', cls=qt.QtPort)
+
+    def test_chromium_mac(self):
+        self.assert_port(port_name='chromium-mac', os_name='mac', os_version='snowleopard',
+                         cls=chromium_mac.ChromiumMacPort)
+        self.assert_port(port_name='chromium', os_name='mac', os_version='lion',
+                         cls=chromium_mac.ChromiumMacPort)
+
+    def test_chromium_linux(self):
+        self.assert_port(port_name='chromium-linux', cls=chromium_linux.ChromiumLinuxPort)
+        self.assert_port(port_name='chromium', os_name='linux', os_version='lucid',
+                         cls=chromium_linux.ChromiumLinuxPort)
+
+    def test_chromium_android(self):
+        self.assert_port(port_name='chromium-android', cls=chromium_android.ChromiumAndroidPort)
+        # NOTE: We can't check for port_name=chromium here, as this will append the host's
+        # operating system, whereas host!=target for Android.
+
+    def test_chromium_win(self):
+        self.assert_port(port_name='chromium-win-xp', cls=chromium_win.ChromiumWinPort)
+        self.assert_port(port_name='chromium-win', os_name='win', os_version='xp',
+                         cls=chromium_win.ChromiumWinPort)
+        self.assert_port(port_name='chromium', os_name='win', os_version='xp',
+                         cls=chromium_win.ChromiumWinPort)
+
+    def test_unknown_specified(self):
+        self.assertRaises(NotImplementedError, factory.PortFactory(MockSystemHost()).get, port_name='unknown')
+
+    def test_unknown_default(self):
+        self.assertRaises(NotImplementedError, factory.PortFactory(MockSystemHost(os_name='vms')).get)
+
+    def test_get_from_builder_name(self):
+        self.assertEquals(factory.PortFactory(MockSystemHost()).get_from_builder_name('WebKit Mac10.7').name(),
+                          'chromium-mac-lion')
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/gtk.py b/Tools/Scripts/webkitpy/layout_tests/port/gtk.py
new file mode 100644
index 0000000..3d82027
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/gtk.py
@@ -0,0 +1,162 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import subprocess
+
+from webkitpy.layout_tests.models.test_configuration import TestConfiguration
+from webkitpy.layout_tests.port.base import Port
+from webkitpy.layout_tests.port.pulseaudio_sanitizer import PulseAudioSanitizer
+from webkitpy.layout_tests.port.xvfbdriver import XvfbDriver
+
+
+class GtkPort(Port, PulseAudioSanitizer):
+    port_name = "gtk"
+
+    def warn_if_bug_missing_in_test_expectations(self):
+        return True
+
+    def _port_flag_for_scripts(self):
+        return "--gtk"
+
+    def _driver_class(self):
+        return XvfbDriver
+
+    def default_timeout_ms(self):
+        # For now, use the base Port's default timeout value in case of WebKitTestRunner.
+        if self.get_option('webkit_test_runner'):
+            return super(GtkPort, self).default_timeout_ms()
+
+        if self.get_option('configuration') == 'Debug':
+            return 12 * 1000
+        return 6 * 1000
+
+    def setup_test_run(self):
+        self._unload_pulseaudio_module()
+
+    def clean_up_test_run(self):
+        super(GtkPort, self).clean_up_test_run()
+        self._restore_pulseaudio_module()
+
+    def setup_environ_for_server(self, server_name=None):
+        environment = super(GtkPort, self).setup_environ_for_server(server_name)
+        environment['GTK_MODULES'] = 'gail'
+        environment['GSETTINGS_BACKEND'] = 'memory'
+        environment['LIBOVERLAY_SCROLLBAR'] = '0'
+        environment['TEST_RUNNER_INJECTED_BUNDLE_FILENAME'] = self._build_path('Libraries', 'libTestRunnerInjectedBundle.la')
+        environment['TEST_RUNNER_TEST_PLUGIN_PATH'] = self._build_path('TestNetscapePlugin', '.libs')
+        environment['WEBKIT_INSPECTOR_PATH'] = self._build_path('Programs', 'resources', 'inspector')
+        environment['AUDIO_RESOURCES_PATH'] = self._filesystem.join(self._config.webkit_base_dir(),
+                                                                    'Source', 'WebCore', 'platform',
+                                                                    'audio', 'resources')
+        self._copy_value_from_environ_if_set(environment, 'WEBKITOUTPUTDIR')
+        return environment
+
+    def _generate_all_test_configurations(self):
+        configurations = []
+        for build_type in self.ALL_BUILD_TYPES:
+            configurations.append(TestConfiguration(version=self._version, architecture='x86', build_type=build_type))
+        return configurations
+
+    def _path_to_driver(self):
+        return self._build_path('Programs', self.driver_name())
+
+    def _path_to_image_diff(self):
+        return self._build_path('Programs', 'ImageDiff')
+
+    def _path_to_webcore_library(self):
+        gtk_library_names = [
+            "libwebkitgtk-1.0.so",
+            "libwebkitgtk-3.0.so",
+            "libwebkit2gtk-1.0.so",
+        ]
+
+        for library in gtk_library_names:
+            full_library = self._build_path(".libs", library)
+            if self._filesystem.isfile(full_library):
+                return full_library
+        return None
+
+    # FIXME: We should find a way to share this implmentation with Gtk,
+    # or teach run-launcher how to call run-safari and move this down to Port.
+    def show_results_html_file(self, results_filename):
+        run_launcher_args = ["file://%s" % results_filename]
+        if self.get_option('webkit_test_runner'):
+            run_launcher_args.append('-2')
+        # FIXME: old-run-webkit-tests also added ["-graphicssystem", "raster", "-style", "windows"]
+        # FIXME: old-run-webkit-tests converted results_filename path for cygwin.
+        self._run_script("run-launcher", run_launcher_args)
+
+    def _get_gdb_output(self, coredump_path):
+        cmd = ['gdb', '-ex', 'thread apply all bt', '--batch', str(self._path_to_driver()), coredump_path]
+        proc = subprocess.Popen(cmd, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        proc.wait()
+        errors = [l.strip().decode('utf8', 'ignore') for l in proc.stderr.readlines()]
+        trace = proc.stdout.read().decode('utf8', 'ignore')
+        return (trace, errors)
+
+    def _get_crash_log(self, name, pid, stdout, stderr, newer_than):
+        pid_representation = str(pid or '<unknown>')
+        log_directory = os.environ.get("WEBKIT_CORE_DUMPS_DIRECTORY")
+        errors = []
+        crash_log = ''
+        expected_crash_dump_filename = "core-pid_%s-_-process_%s" % (pid_representation, name)
+
+        def match_filename(filesystem, directory, filename):
+            if pid:
+                return filename == expected_crash_dump_filename
+            return filename.find(name) > -1
+
+        if log_directory:
+            dumps = self._filesystem.files_under(log_directory, file_filter=match_filename)
+            if dumps:
+                # Get the most recent coredump matching the pid and/or process name.
+                coredump_path = list(reversed(sorted(dumps)))[0]
+                if not newer_than or self._filesystem.mtime(coredump_path) > newer_than:
+                    crash_log, errors = self._get_gdb_output(coredump_path)
+
+        stderr_lines = errors + (stderr or '<empty>').decode('utf8', 'ignore').splitlines()
+        errors_str = '\n'.join(('STDERR: ' + l) for l in stderr_lines)
+        if not crash_log:
+            if not log_directory:
+                log_directory = "/path/to/coredumps"
+            core_pattern = os.path.join(log_directory, "core-pid_%p-_-process_%e")
+            crash_log = """\
+Coredump %(expected_crash_dump_filename)s not found. To enable crash logs:
+
+- run this command as super-user: echo "%(core_pattern)s" > /proc/sys/kernel/core_pattern
+- enable core dumps: ulimit -c unlimited
+- set the WEBKIT_CORE_DUMPS_DIRECTORY environment variable: export WEBKIT_CORE_DUMPS_DIRECTORY=%(log_directory)s
+
+""" % locals()
+
+        return (stderr, """\
+Crash log for %(name)s (pid %(pid_representation)s):
+
+%(crash_log)s
+%(errors_str)s""" % locals())
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/gtk_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/gtk_unittest.py
new file mode 100644
index 0000000..f1df6bf
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/gtk_unittest.py
@@ -0,0 +1,90 @@
+# Copyright (C) 2011 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 os
+
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.layout_tests.port.gtk import GtkPort
+from webkitpy.layout_tests.port import port_testcase
+from webkitpy.common.system.executive_mock import MockExecutive
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.tool.mocktool import MockOptions
+
+
+class GtkPortTest(port_testcase.PortTestCase):
+    port_name = 'gtk'
+    port_maker = GtkPort
+
+    def test_show_results_html_file(self):
+        port = self.make_port()
+        port._executive = MockExecutive(should_log=True)
+        expected_stderr = "MOCK run_command: ['Tools/Scripts/run-launcher', '--release', '--gtk', 'file://test.html'], cwd=/mock-checkout\n"
+        OutputCapture().assert_outputs(self, port.show_results_html_file, ["test.html"], expected_stderr=expected_stderr)
+
+    def test_default_timeout_ms(self):
+        self.assertEquals(self.make_port(options=MockOptions(configuration='Release')).default_timeout_ms(), 6000)
+        self.assertEquals(self.make_port(options=MockOptions(configuration='Debug')).default_timeout_ms(), 12000)
+        self.assertEquals(self.make_port(options=MockOptions(webkit_test_runner=True, configuration='Debug')).default_timeout_ms(), 80000)
+        self.assertEquals(self.make_port(options=MockOptions(webkit_test_runner=True, configuration='Release')).default_timeout_ms(), 80000)
+
+    def assertLinesEqual(self, a, b):
+        if hasattr(self, 'assertMultiLineEqual'):
+            self.assertMultiLineEqual(a, b)
+        else:
+            self.assertEqual(a.splitlines(), b.splitlines())
+
+    def test_get_crash_log(self):
+        core_directory = os.environ.get('WEBKIT_CORE_DUMPS_DIRECTORY', '/path/to/coredumps')
+        core_pattern = os.path.join(core_directory, "core-pid_%p-_-process_%e")
+        mock_empty_crash_log = """\
+Crash log for DumpRenderTree (pid 28529):
+
+Coredump core-pid_28529-_-process_DumpRenderTree not found. To enable crash logs:
+
+- run this command as super-user: echo "%(core_pattern)s" > /proc/sys/kernel/core_pattern
+- enable core dumps: ulimit -c unlimited
+- set the WEBKIT_CORE_DUMPS_DIRECTORY environment variable: export WEBKIT_CORE_DUMPS_DIRECTORY=%(core_directory)s
+
+
+STDERR: <empty>""" % locals()
+
+        def _mock_gdb_output(coredump_path):
+            return (mock_empty_crash_log, [])
+
+        port = self.make_port()
+        port._get_gdb_output = mock_empty_crash_log
+        stderr, log = port._get_crash_log("DumpRenderTree", 28529, "", "", newer_than=None)
+        self.assertEqual(stderr, "")
+        self.assertLinesEqual(log, mock_empty_crash_log)
+
+        stderr, log = port._get_crash_log("DumpRenderTree", 28529, "", "", newer_than=0.0)
+        self.assertEqual(stderr, "")
+        self.assertLinesEqual(log, mock_empty_crash_log)
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/http_lock.py b/Tools/Scripts/webkitpy/layout_tests/port/http_lock.py
new file mode 100644
index 0000000..c2eece3
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/http_lock.py
@@ -0,0 +1,134 @@
+#!/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
+perf, http and websocket tests in a same time."""
+
+import logging
+import os
+import sys
+import tempfile
+import time
+
+from webkitpy.common.system.executive import Executive
+from webkitpy.common.system.file_lock import FileLock
+from webkitpy.common.system.filesystem import FileSystem
+
+
+_log = logging.getLogger(__name__)
+
+
+class HttpLock(object):
+    def __init__(self, lock_path, lock_file_prefix="WebKitHttpd.lock.", guard_lock="WebKit.lock", filesystem=None, executive=None):
+        self._executive = executive or Executive()
+        self._filesystem = filesystem or FileSystem()
+        self._lock_path = lock_path
+        if not self._lock_path:
+            # FIXME: FileSystem should have an accessor for tempdir()
+            self._lock_path = tempfile.gettempdir()
+        self._lock_file_prefix = lock_file_prefix
+        self._lock_file_path_prefix = self._filesystem.join(self._lock_path, self._lock_file_prefix)
+        self._guard_lock_file = self._filesystem.join(self._lock_path, guard_lock)
+        self._guard_lock = FileLock(self._guard_lock_file)
+        self._process_lock_file_name = ""
+
+    def cleanup_http_lock(self):
+        """Delete the lock file if exists."""
+        if self._filesystem.exists(self._process_lock_file_name):
+            _log.debug("Removing lock file: %s" % self._process_lock_file_name)
+            self._filesystem.remove(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 = self._filesystem.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 _current_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:
+            _log.debug("No lock file list")
+            return
+        try:
+            current_pid = self._filesystem.read_text_file(lock_list[0])
+            if not (current_pid and self._executive.check_running_pid(int(current_pid))):
+                _log.debug("Removing stuck lock file: %s" % lock_list[0])
+                self._filesystem.remove(lock_list[0])
+                return
+        except IOError, e:
+            _log.debug("IOError: %s" % e)
+            return
+        except OSError, e:
+            _log.debug("OSError: %s" % e)
+            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 guard lock ensures that the lock numbers are
+        sequential."""
+        if not self._filesystem.exists(self._lock_path):
+            _log.debug("Lock directory does not exist: %s" % self._lock_path)
+            return False
+
+        if not self._guard_lock.acquire_lock():
+            _log.debug("Guard lock timed out!")
+            return False
+
+        self._process_lock_file_name = (self._lock_file_path_prefix + str(self._next_lock_number()))
+        _log.debug("Creating lock file: %s" % self._process_lock_file_name)
+        # FIXME: Executive.py should have an accessor for getpid()
+        self._filesystem.write_text_file(self._process_lock_file_name, str(os.getpid()))
+        self._guard_lock.release_lock()
+        return True
+
+    def wait_for_httpd_lock(self):
+        """Create a lock file and wait until it's turn comes. If something goes wrong
+        it wont do any locking."""
+        if not self._create_lock_file():
+            _log.debug("Warning, http locking failed!")
+            return
+
+        # FIXME: This can hang forever!
+        while self._current_lock_pid() != os.getpid():
+            time.sleep(1)
+
+        _log.debug("HTTP lock acquired")
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/http_lock_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/http_lock_unittest.py
new file mode 100644
index 0000000..fbf2d9d
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/http_lock_unittest.py
@@ -0,0 +1,121 @@
+#!/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.
+
+from http_lock import HttpLock
+import os  # Used for os.getpid()
+import unittest
+
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.executive_mock import MockExecutive
+
+
+# FIXME: These tests all touch the real disk, but could be written to a MockFileSystem instead.
+class HttpLockTestWithRealFileSystem(unittest.TestCase):
+    # FIXME: Unit tests do not use an __init__ method, but rather setUp and tearDown methods.
+    def __init__(self, testFunc):
+        self.http_lock = HttpLock(None, "WebKitTestHttpd.lock.", "WebKitTest.lock")
+        self.filesystem = self.http_lock._filesystem  # FIXME: We should be passing in a MockFileSystem instead.
+        self.lock_file_path_prefix = self.filesystem.join(self.http_lock._lock_path, self.http_lock._lock_file_prefix)
+        self.lock_file_name = self.lock_file_path_prefix + "0"
+        self.guard_lock_file = self.http_lock._guard_lock_file
+        self.clean_all_lockfile()
+        unittest.TestCase.__init__(self, testFunc)
+
+    def clean_all_lockfile(self):
+        if self.filesystem.exists(self.guard_lock_file):
+            self.filesystem.remove(self.guard_lock_file)
+        lock_list = self.filesystem.glob(self.lock_file_path_prefix + '*')
+        for file_name in lock_list:
+            self.filesystem.remove(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 self.filesystem.exists(self.lock_file_name):
+            pid = os.getpid()
+            lock_file_pid = self.filesystem.read_text_file(self.lock_file_name)
+            self.assertEqual(pid, int(lock_file_pid))
+            return True
+        return False
+
+    def test_lock_lifecycle(self):
+        self.http_lock._create_lock_file()
+
+        self.assertEqual(True, self._check_lock_file())
+        self.assertEqual(1, self.http_lock._next_lock_number())
+
+        self.http_lock.cleanup_http_lock()
+
+        self.assertEqual(False, self._check_lock_file())
+        self.assertEqual(0, self.http_lock._next_lock_number())
+
+
+class HttpLockTest(unittest.TestCase):
+    def setUp(self):
+        self.filesystem = MockFileSystem()
+        self.http_lock = HttpLock(None, "WebKitTestHttpd.lock.", "WebKitTest.lock", filesystem=self.filesystem, executive=MockExecutive())
+        # FIXME: Shouldn't we be able to get these values from the http_lock object directly?
+        self.lock_file_path_prefix = self.filesystem.join(self.http_lock._lock_path, self.http_lock._lock_file_prefix)
+        self.lock_file_name = self.lock_file_path_prefix + "0"
+
+    def test_current_lock_pid(self):
+        # FIXME: Once Executive wraps getpid, we can mock this and not use a real pid.
+        current_pid = os.getpid()
+        self.http_lock._filesystem.write_text_file(self.lock_file_name, str(current_pid))
+        self.assertEquals(self.http_lock._current_lock_pid(), current_pid)
+
+    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._extract_lock_number(lock_file), expected)
+
+    def test_lock_file_list(self):
+        self.http_lock._filesystem = MockFileSystem({
+            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",
+        ]
+
+        self.assertEqual(self.http_lock._lock_file_list(), expected_file_list)
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/image_diff.py b/Tools/Scripts/webkitpy/layout_tests/port/image_diff.py
new file mode 100644
index 0000000..72d061f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/image_diff.py
@@ -0,0 +1,119 @@
+#!/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
+# Copyright (C) 2011, 2012 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:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""WebKit implementations of the Port interface."""
+
+import logging
+import re
+import time
+
+from webkitpy.layout_tests.port import server_process
+
+
+_log = logging.getLogger(__name__)
+
+
+class ImageDiffer(object):
+    def __init__(self, port):
+        self._port = port
+        self._tolerance = None
+        self._process = None
+
+    def diff_image(self, expected_contents, actual_contents, tolerance):
+        if tolerance != self._tolerance:
+            self.stop()
+        try:
+            assert(expected_contents)
+            assert(actual_contents)
+            assert(tolerance is not None)
+
+            if not self._process:
+                self._start(tolerance)
+            # Note that although we are handed 'old', 'new', ImageDiff wants 'new', 'old'.
+            self._process.write('Content-Length: %d\n%sContent-Length: %d\n%s' % (
+                len(actual_contents), actual_contents,
+                len(expected_contents), expected_contents))
+            return self._read()
+        except IOError as exception:
+            return (None, 0, "Failed to compute an image diff: %s" % str(exception))
+
+    def _start(self, tolerance):
+        command = [self._port._path_to_image_diff(), '--tolerance', str(tolerance)]
+        environment = self._port.setup_environ_for_server('ImageDiff')
+        self._process = self._port._server_process_constructor(self._port, 'ImageDiff', command, environment)
+        self._process.start()
+        self._tolerance = tolerance
+
+    def _read(self):
+        deadline = time.time() + 2.0
+        output = None
+        output_image = ""
+
+        while not self._process.timed_out and not self._process.has_crashed():
+            output = self._process.read_stdout_line(deadline)
+            if self._process.timed_out or self._process.has_crashed() or not output:
+                break
+
+            if output.startswith('diff'):  # This is the last line ImageDiff prints.
+                break
+
+            if output.startswith('Content-Length'):
+                m = re.match('Content-Length: (\d+)', output)
+                content_length = int(m.group(1))
+                output_image = self._process.read_stdout(deadline, content_length)
+                output = self._process.read_stdout_line(deadline)
+                break
+
+        stderr = self._process.pop_all_buffered_stderr()
+        err_str = ''
+        if stderr:
+            err_str += "ImageDiff produced stderr output:\n" + stderr
+        if self._process.timed_out:
+            err_str += "ImageDiff timed out\n"
+        if self._process.has_crashed():
+            err_str += "ImageDiff crashed\n"
+
+        # FIXME: There is no need to shut down the ImageDiff server after every diff.
+        self._process.stop()
+
+        diff_percent = 0
+        if output and output.startswith('diff'):
+            m = re.match('diff: (.+)% (passed|failed)', output)
+            if m.group(2) == 'passed':
+                return (None, 0, None)
+            diff_percent = float(m.group(1))
+
+        return (output_image, diff_percent, err_str or None)
+
+    def stop(self):
+        if self._process:
+            self._process.stop()
+            self._process = None
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/image_diff_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/image_diff_unittest.py
new file mode 100755
index 0000000..46cc98a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/image_diff_unittest.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2012 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.
+
+"""Unit testing base class for Port implementations."""
+
+import unittest
+
+from webkitpy.layout_tests.port.server_process_mock import MockServerProcess
+from webkitpy.layout_tests.port.image_diff import ImageDiffer
+
+
+class FakePort(object):
+    def __init__(self, server_process_output):
+        self._server_process_constructor = lambda port, nm, cmd, env: MockServerProcess(lines=server_process_output)
+
+    def _path_to_image_diff(self):
+        return ''
+
+    def setup_environ_for_server(self, nm):
+        return None
+
+
+class TestImageDiffer(unittest.TestCase):
+    def test_diff_image_failed(self):
+        port = FakePort(['diff: 100% failed\n'])
+        image_differ = ImageDiffer(port)
+        self.assertEquals(image_differ.diff_image('foo', 'bar', 0.1), ('', 100.0, None))
+
+    def test_diff_image_passed(self):
+        port = FakePort(['diff: 0% passed\n'])
+        image_differ = ImageDiffer(port)
+        self.assertEquals(image_differ.diff_image('foo', 'bar', 0.1), (None, 0, None))
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/leakdetector.py b/Tools/Scripts/webkitpy/layout_tests/port/leakdetector.py
new file mode 100644
index 0000000..f46cd34
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/leakdetector.py
@@ -0,0 +1,153 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import logging
+import re
+
+from webkitpy.common.system.executive import ScriptError
+
+_log = logging.getLogger(__name__)
+
+
+# If other ports/platforms decide to support --leaks, we should see about sharing as much of this code as possible.
+# Right now this code is only used by Apple's MacPort.
+
+class LeakDetector(object):
+    def __init__(self, port):
+        # We should operate on a "platform" not a port here.
+        self._port = port
+        self._executive = port._executive
+        self._filesystem = port._filesystem
+
+    # We exclude the following reported leaks so they do not get in our way when looking for WebKit leaks:
+    # This allows us ignore known leaks and only be alerted when new leaks occur. Some leaks are in the old
+    # versions of the system frameworks that are being used by the leaks bots. Even though a leak has been
+    # fixed, it will be listed here until the bot has been updated with the newer frameworks.
+    def _types_to_exlude_from_leaks(self):
+        # Currently we don't have any type excludes from OS leaks, but we will likely again in the future.
+        return []
+
+    def _callstacks_to_exclude_from_leaks(self):
+        callstacks = [
+            "Flash_EnforceLocalSecurity",  # leaks in Flash plug-in code, rdar://problem/4449747
+            "ScanFromString", # <http://code.google.com/p/angleproject/issues/detail?id=249> leak in ANGLE
+        ]
+        if self._port.is_snowleopard():
+            callstacks += [
+                "readMakerNoteProps",  # <rdar://problem/7156432> leak in ImageIO
+                "QTKitMovieControllerView completeUISetup",  # <rdar://problem/7155156> leak in QTKit
+                "getVMInitArgs",  # <rdar://problem/7714444> leak in Java
+                "Java_java_lang_System_initProperties",  # <rdar://problem/7714465> leak in Java
+                "glrCompExecuteKernel",  # <rdar://problem/7815391> leak in graphics driver while using OpenGL
+                "NSNumberFormatter getObjectValue:forString:errorDescription:",  # <rdar://problem/7149350> Leak in NSNumberFormatter
+            ]
+        elif self._port.is_lion():
+            callstacks += [
+                "FigByteFlumeCustomURLCreateWithURL", # <rdar://problem/10461926> leak in CoreMedia
+                "PDFPage\(PDFPageInternal\) pageLayoutIfAvail", # <rdar://problem/10462055> leak in PDFKit
+                "SecTransformExecute", # <rdar://problem/10470667> leak in Security.framework
+                "_NSCopyStyleRefForFocusRingStyleClip", # <rdar://problem/10462031> leak in AppKit
+            ]
+        return callstacks
+
+    def _leaks_args(self, pid):
+        leaks_args = []
+        for callstack in self._callstacks_to_exclude_from_leaks():
+            leaks_args += ['--exclude-callstack=%s' % callstack]
+        for excluded_type in self._types_to_exlude_from_leaks():
+            leaks_args += ['--exclude-type=%s' % excluded_type]
+        leaks_args.append(pid)
+        return leaks_args
+
+    def _parse_leaks_output(self, leaks_output):
+        _, count, bytes = re.search(r'Process (?P<pid>\d+): (?P<count>\d+) leaks? for (?P<bytes>\d+) total', leaks_output).groups()
+        excluded_match = re.search(r'(?P<excluded>\d+) leaks? excluded', leaks_output)
+        excluded = excluded_match.group('excluded') if excluded_match else 0
+        return int(count), int(excluded), int(bytes)
+
+    def leaks_files_in_directory(self, directory):
+        return self._filesystem.glob(self._filesystem.join(directory, "*-leaks.txt"))
+
+    def leaks_file_name(self, process_name, process_pid):
+        # We include the number of files this worker has already written in the name to prevent overwritting previous leak results..
+        return "%s-%s-leaks.txt" % (process_name, process_pid)
+
+    def count_total_bytes_and_unique_leaks(self, leak_files):
+        merge_depth = 5  # ORWT had a --merge-leak-depth argument, but that seems out of scope for the run-webkit-tests tool.
+        args = [
+            '--merge-depth',
+            merge_depth,
+        ] + leak_files
+        try:
+            parse_malloc_history_output = self._port._run_script("parse-malloc-history", args, include_configuration_arguments=False)
+        except ScriptError, e:
+            _log.warn("Failed to parse leaks output: %s" % e.message_with_output())
+            return
+
+        # total: 5,888 bytes (0 bytes excluded).
+        unique_leak_count = len(re.findall(r'^(\d*)\scalls', parse_malloc_history_output, re.MULTILINE))
+        total_bytes_string = re.search(r'^total\:\s(.+)\s\(', parse_malloc_history_output, re.MULTILINE).group(1)
+        return (total_bytes_string, unique_leak_count)
+
+    def count_total_leaks(self, leak_file_paths):
+        total_leaks = 0
+        for leak_file_path in leak_file_paths:
+            # Leaks have been seen to include non-utf8 data, so we use read_binary_file.
+            # See https://bugs.webkit.org/show_bug.cgi?id=71112.
+            leaks_output = self._filesystem.read_binary_file(leak_file_path)
+            count, _, _ = self._parse_leaks_output(leaks_output)
+            total_leaks += count
+        return total_leaks
+
+    def check_for_leaks(self, process_name, process_pid):
+        _log.debug("Checking for leaks in %s" % process_name)
+        try:
+            # Oddly enough, run-leaks (or the underlying leaks tool) does not seem to always output utf-8,
+            # thus we pass decode_output=False.  Without this code we've seen errors like:
+            # "UnicodeDecodeError: 'utf8' codec can't decode byte 0x88 in position 779874: unexpected code byte"
+            leaks_output = self._port._run_script("run-leaks", self._leaks_args(process_pid), include_configuration_arguments=False, decode_output=False)
+        except ScriptError, e:
+            _log.warn("Failed to run leaks tool: %s" % e.message_with_output())
+            return
+
+        # FIXME: We end up parsing this output 3 times.  Once here and twice for summarizing.
+        count, excluded, bytes = self._parse_leaks_output(leaks_output)
+        adjusted_count = count - excluded
+        if not adjusted_count:
+            return
+
+        leaks_filename = self.leaks_file_name(process_name, process_pid)
+        leaks_output_path = self._filesystem.join(self._port.results_directory(), leaks_filename)
+        self._filesystem.write_binary_file(leaks_output_path, leaks_output)
+
+        # FIXME: Ideally we would not be logging from the worker process, but rather pass the leak
+        # information back to the manager and have it log.
+        if excluded:
+            _log.info("%s leaks (%s bytes including %s excluded leaks) were found, details in %s" % (adjusted_count, bytes, excluded, leaks_output_path))
+        else:
+            _log.info("%s leaks (%s bytes) were found, details in %s" % (count, bytes, leaks_output_path))
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/leakdetector_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/leakdetector_unittest.py
new file mode 100644
index 0000000..7628bb7
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/leakdetector_unittest.py
@@ -0,0 +1,152 @@
+# Copyright (C) 2011 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.layout_tests.port.leakdetector import LeakDetector
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.system.executive_mock import MockExecutive
+
+
+class LeakDetectorTest(unittest.TestCase):
+    def _mock_port(self):
+        class MockPort(object):
+            def __init__(self):
+                self._filesystem = MockFileSystem()
+                self._executive = MockExecutive()
+
+        return MockPort()
+
+    def _make_detector(self):
+        return LeakDetector(self._mock_port())
+
+    def test_leaks_args(self):
+        detector = self._make_detector()
+        detector._callstacks_to_exclude_from_leaks = lambda: ['foo bar', 'BAZ']
+        detector._types_to_exlude_from_leaks = lambda: ['abcdefg', 'hi jklmno']
+        expected_args = ['--exclude-callstack=foo bar', '--exclude-callstack=BAZ', '--exclude-type=abcdefg', '--exclude-type=hi jklmno', 1234]
+        self.assertEquals(detector._leaks_args(1234), expected_args)
+
+    example_leaks_output = """Process 5122: 663744 nodes malloced for 78683 KB
+Process 5122: 337301 leaks for 6525216 total leaked bytes.
+Leak: 0x38cb600  size=3072  zone: DefaultMallocZone_0x1d94000   instance of 'NSCFData', type ObjC, implemented in Foundation
+        0xa033f0b8 0x01001384 0x00000b3a 0x00000b3a     ..3.....:...:...
+        0x00000000 0x038cb620 0x00000000 0x00000000     .... ...........
+        0x00000000 0x21000000 0x726c6468 0x00000000     .......!hdlr....
+        0x00000000 0x7269646d 0x6c707061 0x00000000     ....mdirappl....
+        0x00000000 0x04000000 0x736c69c1 0x00000074     .........ilst...
+        0x6f74a923 0x0000006f 0x7461641b 0x00000061     #.too....data...
+        0x00000001 0x76614c00 0x2e323566 0x302e3236     .....Lavf52.62.0
+        0x37000000 0x6d616ea9 0x2f000000 0x61746164     ...7.nam.../data
+        ...
+Leak: 0x2a9c960  size=288  zone: DefaultMallocZone_0x1d94000
+        0x09a1cc47 0x1bda8560 0x3d472cd1 0xfbe9bccd     G...`....,G=....
+        0x8bcda008 0x9e972a91 0xa892cf63 0x2448bdb0     .....*..c.....H$
+        0x4736fc34 0xdbe2d94e 0x25f56688 0x839402a4     4.6GN....f.%....
+        0xd12496b3 0x59c40c12 0x8cfcab2a 0xd20ef9c4     ..$....Y*.......
+        0xe7c56b1b 0x5835af45 0xc69115de 0x6923e4bb     .k..E.5X......#i
+        0x86f15553 0x15d40fa9 0x681288a4 0xc33298a9     SU.........h..2.
+        0x439bb535 0xc4fc743d 0x7dfaaff8 0x2cc49a4a     5..C=t.....}J..,
+        0xdd119df8 0x7e086821 0x3d7d129e 0x2e1b1547     ....!h.~..}=G...
+        ...
+Leak: 0x25102fe0  size=176  zone: DefaultMallocZone_0x1d94000   string 'NSException Data'
+"""
+
+    example_leaks_output_with_exclusions = """
+Process 57064: 865808 nodes malloced for 81032 KB
+Process 57064: 282 leaks for 21920 total leaked bytes.
+Leak: 0x7fc506023960  size=576  zone: DefaultMallocZone_0x107c29000   URLConnectionLoader::LoaderConnectionEventQueue  C++  CFNetwork
+        0x73395460 0x00007fff 0x7488af40 0x00007fff     `T9s....@..t....
+        0x73395488 0x00007fff 0x46eecd74 0x0001ed83     .T9s....t..F....
+        0x0100000a 0x00000000 0x7488bfc0 0x00007fff     ...........t....
+        0x00000000 0x00000000 0x46eecd8b 0x0001ed83     ...........F....
+        0x00000000 0x00000000 0x00000000 0x00000000     ................
+        0x00000000 0x00000000 0x46eecda3 0x0001ed83     ...........F....
+        0x00000000 0x00000000 0x00000000 0x00000000     ................
+        0x00000000 0x00000000 0x46eecdbc 0x0001ed83     ...........F....
+        ...
+Leak: 0x7fc506025980  size=432  zone: DefaultMallocZone_0x107c29000   URLConnectionInstanceData  CFType  CFNetwork
+        0x74862b28 0x00007fff 0x00012b80 0x00000001     (+.t.....+......
+        0x73395310 0x00007fff 0x733953f8 0x00007fff     .S9s.....S9s....
+        0x4d555458 0x00000000 0x00000000 0x00002068     XTUM........h ..
+        0x00000000 0x00000000 0x00000b00 0x00000b00     ................
+        0x00000000 0x00000000 0x060259b8 0x00007fc5     .........Y......
+        0x060259bc 0x00007fc5 0x00000000 0x00000000     .Y..............
+        0x73395418 0x00007fff 0x06025950 0x00007fc5     .T9s....PY......
+        0x73395440 0x00007fff 0x00005013 0x00000001     @T9s.....P......
+        ...
+
+
+Binary Images:
+       0x107ac2000 -        0x107b4aff7 +DumpRenderTree (??? - ???) <5694BE03-A60A-30B2-9D40-27CFFCFB88EE> /Volumes/Data/WebKit-BuildSlave/lion-intel-leaks/build/WebKitBuild/Debug/DumpRenderTree
+       0x107c2f000 -        0x107c58fff +libWebCoreTestSupport.dylib (535.8.0 - compatibility 1.0.0) <E4F7A13E-5807-30F7-A399-62F8395F9106> /Volumes/Data/WebKit-BuildSlave/lion-intel-leaks/build/WebKitBuild/Debug/libWebCoreTestSupport.dylib
+17 leaks excluded (not printed)
+"""
+
+    def test_parse_leaks_output(self):
+        self.assertEquals(self._make_detector()._parse_leaks_output(self.example_leaks_output), (337301, 0, 6525216))
+        self.assertEquals(self._make_detector()._parse_leaks_output(self.example_leaks_output_with_exclusions), (282, 17, 21920))
+
+    def test_leaks_files_in_directory(self):
+        detector = self._make_detector()
+        self.assertEquals(detector.leaks_files_in_directory('/bogus-directory'), [])
+        detector._filesystem = MockFileSystem({
+            '/mock-results/DumpRenderTree-1234-leaks.txt': '',
+            '/mock-results/DumpRenderTree-23423-leaks.txt': '',
+            '/mock-results/DumpRenderTree-823-leaks.txt': '',
+        })
+        self.assertEquals(len(detector.leaks_files_in_directory('/mock-results')), 3)
+
+    def test_count_total_bytes_and_unique_leaks(self):
+        detector = self._make_detector()
+
+        def mock_run_script(name, args, include_configuration_arguments=False):
+            print "MOCK _run_script: %s %s" % (name, args)
+            return """1 calls for 16 bytes: -[NSURLRequest mutableCopyWithZone:] | +[NSObject(NSObject) allocWithZone:] | _internal_class_createInstanceFromZone | calloc | malloc_zone_calloc
+
+147 calls for 9,408 bytes: _CFRuntimeCreateInstance | _ZN3WTF24StringWrapperCFAllocatorL8allocateElmPv StringImplCF.cpp:67 | WTF::fastMalloc(unsigned long) FastMalloc.cpp:268 | malloc | malloc_zone_malloc 
+
+total: 5,888 bytes (0 bytes excluded)."""
+        detector._port._run_script = mock_run_script
+
+        leak_files = ['/mock-results/DumpRenderTree-1234-leaks.txt', '/mock-results/DumpRenderTree-1235-leaks.txt']
+        expected_stdout = "MOCK _run_script: parse-malloc-history ['--merge-depth', 5, '/mock-results/DumpRenderTree-1234-leaks.txt', '/mock-results/DumpRenderTree-1235-leaks.txt']\n"
+        results_tuple = OutputCapture().assert_outputs(self, detector.count_total_bytes_and_unique_leaks, [leak_files], expected_stdout=expected_stdout)
+        self.assertEquals(results_tuple, ("5,888 bytes", 2))
+
+    def test_count_total_leaks(self):
+        detector = self._make_detector()
+        detector._filesystem = MockFileSystem({
+            # The \xff is some non-utf8 characters to make sure we don't blow up trying to parse the file.
+            '/mock-results/DumpRenderTree-1234-leaks.txt': '\xff\nProcess 1234: 12 leaks for 40 total leaked bytes.\n\xff\n',
+            '/mock-results/DumpRenderTree-23423-leaks.txt': 'Process 1235: 12341 leaks for 27934 total leaked bytes.\n',
+            '/mock-results/DumpRenderTree-823-leaks.txt': 'Process 12356: 23412 leaks for 18 total leaked bytes.\n',
+        })
+        leak_file_paths = ['/mock-results/DumpRenderTree-1234-leaks.txt', '/mock-results/DumpRenderTree-23423-leaks.txt', '/mock-results/DumpRenderTree-823-leaks.txt']
+        self.assertEquals(detector.count_total_leaks(leak_file_paths), 35765)
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/mac.py b/Tools/Scripts/webkitpy/layout_tests/port/mac.py
new file mode 100644
index 0000000..e6fd5bd
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/mac.py
@@ -0,0 +1,280 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+# Copyright (C) 2012 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:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import logging
+import os
+import re
+import subprocess
+import sys
+import time
+
+from webkitpy.common.system.crashlogs import CrashLogs
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.layout_tests.port.apple import ApplePort
+from webkitpy.layout_tests.port.leakdetector import LeakDetector
+
+
+_log = logging.getLogger(__name__)
+
+
+class MacPort(ApplePort):
+    port_name = "mac"
+
+    VERSION_FALLBACK_ORDER = ['mac-snowleopard', 'mac-lion', 'mac-mountainlion']
+
+    ARCHITECTURES = ['x86_64', 'x86']
+
+    def __init__(self, host, port_name, **kwargs):
+        ApplePort.__init__(self, host, port_name, **kwargs)
+        self._architecture = self.get_option('architecture')
+
+        if not self._architecture:
+            self._architecture = 'x86_64'
+
+        self._leak_detector = LeakDetector(self)
+        if self.get_option("leaks"):
+            # DumpRenderTree slows down noticably if we run more than about 1000 tests in a batch
+            # with MallocStackLogging enabled.
+            self.set_option_default("batch_size", 1000)
+
+    def default_timeout_ms(self):
+        if self.get_option('guard_malloc'):
+            return 350 * 1000
+        return super(MacPort, self).default_timeout_ms()
+
+    def _build_driver_flags(self):
+        return ['ARCHS=i386'] if self.architecture() == 'x86' else []
+
+    def should_retry_crashes(self):
+        # On Apple Mac, we retry crashes due to https://bugs.webkit.org/show_bug.cgi?id=82233
+        return True
+
+    def default_baseline_search_path(self):
+        if self._name.endswith(self.FUTURE_VERSION):
+            fallback_names = [self.port_name]
+        else:
+            fallback_names = self.VERSION_FALLBACK_ORDER[self.VERSION_FALLBACK_ORDER.index(self._name):-1] + [self.port_name]
+        if self.get_option('webkit_test_runner'):
+            fallback_names.insert(0, self._wk2_port_name())
+            # Note we do not add 'wk2' here, even though it's included in _skipped_search_paths().
+        return map(self._webkit_baseline_path, fallback_names)
+
+    def setup_environ_for_server(self, server_name=None):
+        env = super(MacPort, self).setup_environ_for_server(server_name)
+        if server_name == self.driver_name():
+            if self.get_option('leaks'):
+                env['MallocStackLogging'] = '1'
+            if self.get_option('guard_malloc'):
+                env['DYLD_INSERT_LIBRARIES'] = '/usr/lib/libgmalloc.dylib'
+        env['XML_CATALOG_FILES'] = ''  # work around missing /etc/catalog <rdar://problem/4292995>
+        return env
+
+    def operating_system(self):
+        return 'mac'
+
+    # Belongs on a Platform object.
+    def is_snowleopard(self):
+        return self._version == "snowleopard"
+
+    # Belongs on a Platform object.
+    def is_lion(self):
+        return self._version == "lion"
+
+    def default_child_processes(self):
+        # FIXME: The Printer isn't initialized when this is called, so using _log would just show an unitialized logger error.
+
+        if self._version == "snowleopard":
+            print >> sys.stderr, "Cannot run tests in parallel on Snow Leopard due to rdar://problem/10621525."
+            return 1
+
+        default_count = super(MacPort, self).default_child_processes()
+
+        # FIXME: https://bugs.webkit.org/show_bug.cgi?id=95906  With too many WebProcess WK2 tests get stuck in resource contention.
+        # To alleviate the issue reduce the number of running processes
+        # Anecdotal evidence suggests that a 4 core/8 core logical machine may run into this, but that a 2 core/4 core logical machine does not.
+        if self.get_option('webkit_test_runner') and default_count > 4:
+            default_count = int(.75 * default_count)
+
+        # Make sure we have enough ram to support that many instances:
+        total_memory = self.host.platform.total_bytes_memory()
+        bytes_per_drt = 256 * 1024 * 1024  # Assume each DRT needs 256MB to run.
+        overhead = 2048 * 1024 * 1024  # Assume we need 2GB free for the O/S
+        supportable_instances = max((total_memory - overhead) / bytes_per_drt, 1)  # Always use one process, even if we don't have space for it.
+        if supportable_instances < default_count:
+            print >> sys.stderr, "This machine could support %s child processes, but only has enough memory for %s." % (default_count, supportable_instances)
+        return min(supportable_instances, default_count)
+
+    def _build_java_test_support(self):
+        java_tests_path = self._filesystem.join(self.layout_tests_dir(), "java")
+        build_java = ["/usr/bin/make", "-C", java_tests_path]
+        if self._executive.run_command(build_java, return_exit_code=True):  # Paths are absolute, so we don't need to set a cwd.
+            _log.error("Failed to build Java support files: %s" % build_java)
+            return False
+        return True
+
+    def check_for_leaks(self, process_name, process_pid):
+        if not self.get_option('leaks'):
+            return
+        # We could use http://code.google.com/p/psutil/ to get the process_name from the pid.
+        self._leak_detector.check_for_leaks(process_name, process_pid)
+
+    def print_leaks_summary(self):
+        if not self.get_option('leaks'):
+            return
+        # We're in the manager process, so the leak detector will not have a valid list of leak files.
+        # FIXME: This is a hack, but we don't have a better way to get this information from the workers yet.
+        # FIXME: This will include too many leaks in subsequent runs until the results directory is cleared!
+        leaks_files = self._leak_detector.leaks_files_in_directory(self.results_directory())
+        if not leaks_files:
+            return
+        total_bytes_string, unique_leaks = self._leak_detector.count_total_bytes_and_unique_leaks(leaks_files)
+        total_leaks = self._leak_detector.count_total_leaks(leaks_files)
+        _log.info("%s total leaks found for a total of %s!" % (total_leaks, total_bytes_string))
+        _log.info("%s unique leaks found!" % unique_leaks)
+
+    def _check_port_build(self):
+        return self._build_java_test_support()
+
+    def _path_to_webcore_library(self):
+        return self._build_path('WebCore.framework/Versions/A/WebCore')
+
+    def show_results_html_file(self, results_filename):
+        # We don't use self._run_script() because we don't want to wait for the script
+        # to exit and we want the output to show up on stdout in case there are errors
+        # launching the browser.
+        self._executive.popen([self._config.script_path('run-safari')] + self._arguments_for_configuration() + ['--no-saved-state', '-NSOpen', results_filename],
+            cwd=self._config.webkit_base_dir(), stdout=file(os.devnull), stderr=file(os.devnull))
+
+    # FIXME: The next two routines turn off the http locking in order
+    # to work around failures on the bots caused when the slave restarts.
+    # See https://bugs.webkit.org/show_bug.cgi?id=64886 for more info.
+    # The proper fix is to make sure the slave is actually stopping NRWT
+    # properly on restart. Note that by removing the lock file and not waiting,
+    # the result should be that if there is a web server already running,
+    # it'll be killed and this one will be started in its place; this
+    # may lead to weird things happening in the other run. However, I don't
+    # think we're (intentionally) actually running multiple runs concurrently
+    # on any Mac bots.
+
+    def acquire_http_lock(self):
+        pass
+
+    def release_http_lock(self):
+        pass
+
+    def _get_crash_log(self, name, pid, stdout, stderr, newer_than, time_fn=None, sleep_fn=None, wait_for_log=True):
+        # Note that we do slow-spin here and wait, since it appears the time
+        # ReportCrash takes to actually write and flush the file varies when there are
+        # lots of simultaneous crashes going on.
+        # FIXME: Should most of this be moved into CrashLogs()?
+        time_fn = time_fn or time.time
+        sleep_fn = sleep_fn or time.sleep
+        crash_log = ''
+        crash_logs = CrashLogs(self.host)
+        now = time_fn()
+        # FIXME: delete this after we're sure this code is working ...
+        _log.debug('looking for crash log for %s:%s' % (name, str(pid)))
+        deadline = now + 5 * int(self.get_option('child_processes', 1))
+        while not crash_log and now <= deadline:
+            crash_log = crash_logs.find_newest_log(name, pid, include_errors=True, newer_than=newer_than)
+            if not wait_for_log:
+                break
+            if not crash_log or not [line for line in crash_log.splitlines() if not line.startswith('ERROR')]:
+                sleep_fn(0.1)
+                now = time_fn()
+
+        if not crash_log:
+            return (stderr, None)
+        return (stderr, crash_log)
+
+    def look_for_new_crash_logs(self, crashed_processes, start_time):
+        """Since crash logs can take a long time to be written out if the system is
+           under stress do a second pass at the end of the test run.
+
+           crashes: test_name -> pid, process_name tuple of crashed process
+           start_time: time the tests started at.  We're looking for crash
+               logs after that time.
+        """
+        crash_logs = {}
+        for (test_name, process_name, pid) in crashed_processes:
+            # Passing None for output.  This is a second pass after the test finished so
+            # if the output had any loggine we would have already collected it.
+            crash_log = self._get_crash_log(process_name, pid, None, None, start_time, wait_for_log=False)[1]
+            if not crash_log:
+                continue
+            crash_logs[test_name] = crash_log
+        return crash_logs
+
+    def sample_process(self, name, pid):
+        try:
+            hang_report = self._filesystem.join(self.results_directory(), "%s-%s.sample.txt" % (name, pid))
+            self._executive.run_command([
+                "/usr/bin/sample",
+                pid,
+                10,
+                10,
+                "-file",
+                hang_report,
+            ])
+        except ScriptError, e:
+            _log.warning('Unable to sample process.')
+
+    def _path_to_helper(self):
+        binary_name = 'LayoutTestHelper'
+        return self._build_path(binary_name)
+
+    def start_helper(self):
+        helper_path = self._path_to_helper()
+        if helper_path:
+            _log.debug("Starting layout helper %s" % helper_path)
+            # Note: Not thread safe: http://bugs.python.org/issue2320
+            self._helper = self._executive.popen([helper_path],
+                stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None)
+            is_ready = self._helper.stdout.readline()
+            if not is_ready.startswith('ready'):
+                _log.error("LayoutTestHelper failed to be ready")
+
+    def stop_helper(self):
+        if self._helper:
+            _log.debug("Stopping LayoutTestHelper")
+            try:
+                self._helper.stdin.write("x\n")
+                self._helper.stdin.close()
+                self._helper.wait()
+            except IOError, e:
+                _log.debug("IOError raised while stopping helper: %s" % str(e))
+                pass
+            self._helper = None
+
+    def nm_command(self):
+        try:
+            return self._executive.run_command(['xcrun', '-find', 'nm']).rstrip()
+        except ScriptError, e:
+            _log.warn("xcrun failed; falling back to 'nm'.")
+            return 'nm'
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/mac_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/mac_unittest.py
new file mode 100644
index 0000000..c2b26b2
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/mac_unittest.py
@@ -0,0 +1,257 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.layout_tests.port.mac import MacPort
+from webkitpy.layout_tests.port import port_testcase
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.tool.mocktool import MockOptions
+from webkitpy.common.system.executive_mock import MockExecutive, MockExecutive2, MockProcess, ScriptError
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+
+
+class MacTest(port_testcase.PortTestCase):
+    os_name = 'mac'
+    os_version = 'lion'
+    port_name = 'mac-lion'
+    port_maker = MacPort
+
+    def assert_skipped_file_search_paths(self, port_name, expected_paths, use_webkit2=False):
+        port = self.make_port(port_name=port_name, options=MockOptions(webkit_test_runner=use_webkit2))
+        self.assertEqual(port._skipped_file_search_paths(), expected_paths)
+
+    def test_default_timeout_ms(self):
+        super(MacTest, self).test_default_timeout_ms()
+        self.assertEquals(self.make_port(options=MockOptions(guard_malloc=True)).default_timeout_ms(), 350000)
+
+
+    example_skipped_file = u"""
+# <rdar://problem/5647952> fast/events/mouseout-on-window.html needs mac DRT to issue mouse out events
+fast/events/mouseout-on-window.html
+
+# <rdar://problem/5643675> window.scrollTo scrolls a window with no scrollbars
+fast/events/attempt-scroll-with-no-scrollbars.html
+
+# see bug <rdar://problem/5646437> REGRESSION (r28015): svg/batik/text/smallFonts fails
+svg/batik/text/smallFonts.svg
+
+# Java tests don't work on WK2
+java/
+"""
+    example_skipped_tests = [
+        "fast/events/mouseout-on-window.html",
+        "fast/events/attempt-scroll-with-no-scrollbars.html",
+        "svg/batik/text/smallFonts.svg",
+        "java",
+    ]
+
+    def test_tests_from_skipped_file_contents(self):
+        port = self.make_port()
+        self.assertEqual(port._tests_from_skipped_file_contents(self.example_skipped_file), self.example_skipped_tests)
+
+    def assert_name(self, port_name, os_version_string, expected):
+        host = MockSystemHost(os_name='mac', os_version=os_version_string)
+        port = self.make_port(host=host, port_name=port_name)
+        self.assertEquals(expected, port.name())
+
+    def test_tests_for_other_platforms(self):
+        platforms = ['mac', 'chromium-linux', 'mac-snowleopard']
+        port = self.make_port(port_name='mac-snowleopard')
+        platform_dir_paths = map(port._webkit_baseline_path, platforms)
+        # Replace our empty mock file system with one which has our expected platform directories.
+        port._filesystem = MockFileSystem(dirs=platform_dir_paths)
+
+        dirs_to_skip = port._tests_for_other_platforms()
+        self.assertTrue('platform/chromium-linux' in dirs_to_skip)
+        self.assertFalse('platform/mac' in dirs_to_skip)
+        self.assertFalse('platform/mac-snowleopard' in dirs_to_skip)
+
+    def test_version(self):
+        port = self.make_port()
+        self.assertTrue(port.version())
+
+    def test_versions(self):
+        # Note: these tests don't need to be exhaustive as long as we get path coverage.
+        self.assert_name('mac', 'snowleopard', 'mac-snowleopard')
+        self.assert_name('mac-snowleopard', 'leopard', 'mac-snowleopard')
+        self.assert_name('mac-snowleopard', 'lion', 'mac-snowleopard')
+
+        self.assert_name('mac', 'lion', 'mac-lion')
+        self.assert_name('mac-lion', 'lion', 'mac-lion')
+
+        self.assert_name('mac', 'mountainlion', 'mac-mountainlion')
+        self.assert_name('mac-mountainlion', 'lion', 'mac-mountainlion')
+
+        self.assert_name('mac', 'future', 'mac-future')
+        self.assert_name('mac-future', 'future', 'mac-future')
+
+        self.assertRaises(AssertionError, self.assert_name, 'mac-tiger', 'leopard', 'mac-leopard')
+
+    def test_setup_environ_for_server(self):
+        port = self.make_port(options=MockOptions(leaks=True, guard_malloc=True))
+        env = port.setup_environ_for_server(port.driver_name())
+        self.assertEquals(env['MallocStackLogging'], '1')
+        self.assertEquals(env['DYLD_INSERT_LIBRARIES'], '/usr/lib/libgmalloc.dylib')
+
+    def _assert_search_path(self, port_name, baseline_path, search_paths, use_webkit2=False):
+        port = self.make_port(port_name=port_name, options=MockOptions(webkit_test_runner=use_webkit2))
+        absolute_search_paths = map(port._webkit_baseline_path, search_paths)
+        self.assertEquals(port.baseline_path(), port._webkit_baseline_path(baseline_path))
+        self.assertEquals(port.baseline_search_path(), absolute_search_paths)
+
+    def test_baseline_search_path(self):
+        # Note that we don't need total coverage here, just path coverage, since this is all data driven.
+        self._assert_search_path('mac-snowleopard', 'mac-snowleopard', ['mac-snowleopard', 'mac-lion', 'mac'])
+        self._assert_search_path('mac-lion', 'mac-lion', ['mac-lion', 'mac'])
+        self._assert_search_path('mac-mountainlion', 'mac', ['mac'])
+        self._assert_search_path('mac-future', 'mac', ['mac'])
+        self._assert_search_path('mac-snowleopard', 'mac-wk2', ['mac-wk2', 'mac-snowleopard', 'mac-lion', 'mac'], use_webkit2=True)
+        self._assert_search_path('mac-lion', 'mac-wk2', ['mac-wk2', 'mac-lion', 'mac'], use_webkit2=True)
+        self._assert_search_path('mac-mountainlion', 'mac-wk2', ['mac-wk2', 'mac'], use_webkit2=True)
+        self._assert_search_path('mac-future', 'mac-wk2', ['mac-wk2', 'mac'], use_webkit2=True)
+
+    def test_show_results_html_file(self):
+        port = self.make_port()
+        # Delay setting a should_log executive to avoid logging from MacPort.__init__.
+        port._executive = MockExecutive(should_log=True)
+        expected_stderr = "MOCK popen: ['Tools/Scripts/run-safari', '--release', '--no-saved-state', '-NSOpen', 'test.html'], cwd=/mock-checkout\n"
+        OutputCapture().assert_outputs(self, port.show_results_html_file, ["test.html"], expected_stderr=expected_stderr)
+
+    def test_operating_system(self):
+        self.assertEqual('mac', self.make_port().operating_system())
+
+    def test_default_child_processes(self):
+        port = self.make_port(port_name='mac-lion')
+        # MockPlatformInfo only has 2 mock cores.  The important part is that 2 > 1.
+        self.assertEqual(port.default_child_processes(), 2)
+
+        bytes_for_drt = 200 * 1024 * 1024
+        port.host.platform.total_bytes_memory = lambda: bytes_for_drt
+        expected_stderr = "This machine could support 2 child processes, but only has enough memory for 1.\n"
+        child_processes = OutputCapture().assert_outputs(self, port.default_child_processes, (), expected_stderr=expected_stderr)
+        self.assertEqual(child_processes, 1)
+
+        # Make sure that we always use one process, even if we don't have the memory for it.
+        port.host.platform.total_bytes_memory = lambda: bytes_for_drt - 1
+        expected_stderr = "This machine could support 2 child processes, but only has enough memory for 1.\n"
+        child_processes = OutputCapture().assert_outputs(self, port.default_child_processes, (), expected_stderr=expected_stderr)
+        self.assertEqual(child_processes, 1)
+
+        # SnowLeopard has a CFNetwork bug which causes crashes if we execute more than one copy of DRT at once.
+        port = self.make_port(port_name='mac-snowleopard')
+        expected_stderr = "Cannot run tests in parallel on Snow Leopard due to rdar://problem/10621525.\n"
+        child_processes = OutputCapture().assert_outputs(self, port.default_child_processes, (), expected_stderr=expected_stderr)
+        self.assertEqual(child_processes, 1)
+
+    def test_get_crash_log(self):
+        # Mac crash logs are tested elsewhere, so here we just make sure we don't crash.
+        def fake_time_cb():
+            times = [0, 20, 40]
+            return lambda: times.pop(0)
+        port = self.make_port(port_name='mac-snowleopard')
+        port._get_crash_log('DumpRenderTree', 1234, '', '', 0,
+            time_fn=fake_time_cb(), sleep_fn=lambda delay: None)
+
+    def test_helper_starts(self):
+        host = MockSystemHost(MockExecutive())
+        port = self.make_port(host)
+        oc = OutputCapture()
+        oc.capture_output()
+        host.executive._proc = MockProcess('ready\n')
+        port.start_helper()
+        port.stop_helper()
+        oc.restore_output()
+
+        # make sure trying to stop the helper twice is safe.
+        port.stop_helper()
+
+    def test_helper_fails_to_start(self):
+        host = MockSystemHost(MockExecutive())
+        port = self.make_port(host)
+        oc = OutputCapture()
+        oc.capture_output()
+        port.start_helper()
+        port.stop_helper()
+        oc.restore_output()
+
+    def test_helper_fails_to_stop(self):
+        host = MockSystemHost(MockExecutive())
+        host.executive._proc = MockProcess()
+
+        def bad_waiter():
+            raise IOError('failed to wait')
+        host.executive._proc.wait = bad_waiter
+
+        port = self.make_port(host)
+        oc = OutputCapture()
+        oc.capture_output()
+        port.start_helper()
+        port.stop_helper()
+        oc.restore_output()
+
+    def test_sample_process(self):
+
+        def logging_run_command(args):
+            print args
+
+        port = self.make_port()
+        port._executive = MockExecutive2(run_command_fn=logging_run_command)
+        expected_stdout = "['/usr/bin/sample', 42, 10, 10, '-file', '/mock-build/layout-test-results/test-42.sample.txt']\n"
+        OutputCapture().assert_outputs(self, port.sample_process, args=['test', 42], expected_stdout=expected_stdout)
+
+    def test_sample_process_throws_exception(self):
+
+        def throwing_run_command(args):
+            raise ScriptError("MOCK script error")
+
+        port = self.make_port()
+        port._executive = MockExecutive2(run_command_fn=throwing_run_command)
+        OutputCapture().assert_outputs(self, port.sample_process, args=['test', 42])
+
+    def test_32bit(self):
+        port = self.make_port(options=MockOptions(architecture='x86'))
+
+        def run_script(script, args=None, env=None):
+            self.args = args
+
+        port._run_script = run_script
+        self.assertEquals(port.architecture(), 'x86')
+        port._build_driver()
+        self.assertEquals(self.args, ['ARCHS=i386'])
+
+    def test_64bit(self):
+        # Apple Mac port is 64-bit by default
+        port = self.make_port()
+        self.assertEquals(port.architecture(), 'x86_64')
+
+        def run_script(script, args=None, env=None):
+            self.args = args
+
+        port._run_script = run_script
+        port._build_driver()
+        self.assertEquals(self.args, [])
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/mock_drt.py b/Tools/Scripts/webkitpy/layout_tests/port/mock_drt.py
new file mode 100644
index 0000000..a2106fd
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/mock_drt.py
@@ -0,0 +1,297 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""
+This is an implementation of the Port interface that overrides other
+ports and changes the Driver binary to "MockDRT".
+
+The MockDRT objects emulate what a real DRT would do. In particular, they
+return the output a real DRT would return for a given test, assuming that
+test actually passes (except for reftests, which currently cause the
+MockDRT to crash).
+"""
+
+import base64
+import logging
+import optparse
+import os
+import sys
+
+# Since we execute this script directly as part of the unit tests, we need to ensure
+# that Tools/Scripts is in sys.path for the next imports to work correctly.
+script_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
+if script_dir not in sys.path:
+    sys.path.append(script_dir)
+
+from webkitpy.common.system.systemhost import SystemHost
+from webkitpy.layout_tests.port.driver import DriverInput, DriverOutput, DriverProxy
+from webkitpy.layout_tests.port.factory import PortFactory
+
+_log = logging.getLogger(__name__)
+
+
+class MockDRTPort(object):
+    port_name = 'mock'
+
+    @classmethod
+    def determine_full_port_name(cls, host, options, port_name):
+        return port_name
+
+    def __init__(self, host, port_name, **kwargs):
+        self.__delegate = PortFactory(host).get(port_name.replace('mock-', ''), **kwargs)
+
+    def __getattr__(self, name):
+        return getattr(self.__delegate, name)
+
+    def check_build(self, needs_http):
+        return True
+
+    def check_sys_deps(self, needs_http):
+        return True
+
+    def create_driver(self, worker_number, no_timeout=False):
+        # The magic of the MockDRTPort is that we create a driver that has a
+        # cmd_line() method monkey-patched to invoke this script instead of DRT.
+        return DriverProxy(self, worker_number, self._mocked_driver_maker, pixel_tests=self.get_option('pixel_tests'), no_timeout=no_timeout)
+
+    @staticmethod
+    def _mocked_driver_maker(port, worker_number, pixel_tests, no_timeout=False):
+        path_to_this_file = port.host.filesystem.abspath(__file__.replace('.pyc', '.py'))
+        driver = port.__delegate._driver_class()(port, worker_number, pixel_tests, no_timeout)
+        driver.cmd_line = port._overriding_cmd_line(driver.cmd_line,
+                                                    port.__delegate._path_to_driver(),
+                                                    sys.executable,
+                                                    path_to_this_file,
+                                                    port.__delegate.name())
+        return driver
+
+    @staticmethod
+    def _overriding_cmd_line(original_cmd_line, driver_path, python_exe, this_file, port_name):
+        def new_cmd_line(pixel_tests, per_test_args):
+            cmd_line = original_cmd_line(pixel_tests, per_test_args)
+            index = cmd_line.index(driver_path)
+            cmd_line[index:index + 1] = [python_exe, this_file, '--platform', port_name]
+            return cmd_line
+
+        return new_cmd_line
+
+    def start_helper(self):
+        pass
+
+    def start_http_server(self, number_of_servers):
+        pass
+
+    def start_websocket_server(self):
+        pass
+
+    def acquire_http_lock(self):
+        pass
+
+    def stop_helper(self):
+        pass
+
+    def stop_http_server(self):
+        pass
+
+    def stop_websocket_server(self):
+        pass
+
+    def release_http_lock(self):
+        pass
+
+
+def main(argv, host, stdin, stdout, stderr):
+    """Run the tests."""
+
+    options, args = parse_options(argv)
+    if options.test_shell:
+        drt = MockTestShell(options, args, host, stdin, stdout, stderr)
+    else:
+        drt = MockDRT(options, args, host, stdin, stdout, stderr)
+    return drt.run()
+
+
+def parse_options(argv):
+    # FIXME: We have to do custom arg parsing instead of using the optparse
+    # module.  First, Chromium and non-Chromium DRTs have a different argument
+    # syntax.  Chromium uses --pixel-tests=<path>, and non-Chromium uses
+    # --pixel-tests as a boolean flag. Second, we don't want to have to list
+    # every command line flag DRT accepts, but optparse complains about
+    # unrecognized flags. At some point it might be good to share a common
+    # DRT options class between this file and webkit.py and chromium.py
+    # just to get better type checking.
+    platform_index = argv.index('--platform')
+    platform = argv[platform_index + 1]
+
+    pixel_tests = False
+    pixel_path = None
+    test_shell = '--test-shell' in argv
+    if test_shell:
+        for arg in argv:
+            if arg.startswith('--pixel-tests'):
+                pixel_tests = True
+                pixel_path = arg[len('--pixel-tests='):]
+    else:
+        pixel_tests = '--pixel-tests' in argv
+    options = optparse.Values({'test_shell': test_shell, 'platform': platform, 'pixel_tests': pixel_tests, 'pixel_path': pixel_path})
+    return (options, argv)
+
+
+class MockDRT(object):
+    def __init__(self, options, args, host, stdin, stdout, stderr):
+        self._options = options
+        self._args = args
+        self._host = host
+        self._stdout = stdout
+        self._stdin = stdin
+        self._stderr = stderr
+
+        port_name = None
+        if options.platform:
+            port_name = options.platform
+        self._port = PortFactory(host).get(port_name=port_name, options=options)
+        self._driver = self._port.create_driver(0)
+
+    def run(self):
+        while True:
+            line = self._stdin.readline()
+            if not line:
+                return 0
+            driver_input = self.input_from_line(line)
+            dirname, basename = self._port.split_test(driver_input.test_name)
+            is_reftest = (self._port.reference_files(driver_input.test_name) or
+                          self._port.is_reference_html_file(self._port._filesystem, dirname, basename))
+            output = self.output_for_test(driver_input, is_reftest)
+            self.write_test_output(driver_input, output, is_reftest)
+
+    def input_from_line(self, line):
+        vals = line.strip().split("'")
+        if len(vals) == 1:
+            uri = vals[0]
+            checksum = None
+        else:
+            uri = vals[0]
+            checksum = vals[1]
+        if uri.startswith('http://') or uri.startswith('https://'):
+            test_name = self._driver.uri_to_test(uri)
+        else:
+            test_name = self._port.relative_test_filename(uri)
+
+        return DriverInput(test_name, 0, checksum, self._options.pixel_tests)
+
+    def output_for_test(self, test_input, is_reftest):
+        port = self._port
+        actual_text = port.expected_text(test_input.test_name)
+        actual_audio = port.expected_audio(test_input.test_name)
+        actual_image = None
+        actual_checksum = None
+        if is_reftest:
+            # Make up some output for reftests.
+            actual_text = 'reference text\n'
+            actual_checksum = 'mock-checksum'
+            actual_image = 'blank'
+            if test_input.test_name.endswith('-mismatch.html'):
+                actual_text = 'not reference text\n'
+                actual_checksum = 'not-mock-checksum'
+                actual_image = 'not blank'
+        elif self._options.pixel_tests and test_input.image_hash:
+            actual_checksum = port.expected_checksum(test_input.test_name)
+            actual_image = port.expected_image(test_input.test_name)
+
+        return DriverOutput(actual_text, actual_image, actual_checksum, actual_audio)
+
+    def write_test_output(self, test_input, output, is_reftest):
+        if output.audio:
+            self._stdout.write('Content-Type: audio/wav\n')
+            self._stdout.write('Content-Transfer-Encoding: base64\n')
+            self._stdout.write(base64.b64encode(output.audio))
+        else:
+            self._stdout.write('Content-Type: text/plain\n')
+            # FIXME: Note that we don't ensure there is a trailing newline!
+            # This mirrors actual (Mac) DRT behavior but is a bug.
+            if output.text:
+                self._stdout.write(output.text)
+
+        self._stdout.write('#EOF\n')
+
+        if self._options.pixel_tests and output.image_hash:
+            self._stdout.write('\n')
+            self._stdout.write('ActualHash: %s\n' % output.image_hash)
+            self._stdout.write('ExpectedHash: %s\n' % test_input.image_hash)
+            if output.image_hash != test_input.image_hash:
+                self._stdout.write('Content-Type: image/png\n')
+                self._stdout.write('Content-Length: %s\n' % len(output.image))
+                self._stdout.write(output.image)
+        self._stdout.write('#EOF\n')
+        self._stdout.flush()
+        self._stderr.write('#EOF\n')
+        self._stderr.flush()
+
+
+class MockTestShell(MockDRT):
+    def input_from_line(self, line):
+        vals = line.strip().split()
+        if len(vals) == 3:
+            uri, timeout, checksum = vals
+        else:
+            uri, timeout = vals
+            checksum = None
+
+        test_name = self._driver.uri_to_test(uri)
+        return DriverInput(test_name, timeout, checksum, self._options.pixel_tests)
+
+    def output_for_test(self, test_input, is_reftest):
+        # FIXME: This is a hack to make virtual tests work. Need something more general.
+        original_test_name = test_input.test_name
+        if '--enable-accelerated-2d-canvas' in self._args and 'canvas' in test_input.test_name:
+            test_input.test_name = 'platform/chromium/virtual/gpu/' + test_input.test_name
+        output = super(MockTestShell, self).output_for_test(test_input, is_reftest)
+        test_input.test_name = original_test_name
+        return output
+
+    def write_test_output(self, test_input, output, is_reftest):
+        self._stdout.write("#URL:%s\n" % self._driver.test_to_uri(test_input.test_name))
+        if self._options.pixel_tests and output.image_hash:
+            self._stdout.write("#MD5:%s\n" % output.image_hash)
+            if output.image:
+                self._host.filesystem.maybe_make_directory(self._host.filesystem.dirname(self._options.pixel_path))
+                self._host.filesystem.write_binary_file(self._options.pixel_path, output.image)
+        if output.text:
+            self._stdout.write(output.text)
+
+        if output.text and not output.text.endswith('\n'):
+            self._stdout.write('\n')
+        self._stdout.write('#EOF\n')
+        self._stdout.flush()
+
+
+if __name__ == '__main__':
+    # Note that the Mock in MockDRT refers to the fact that it is emulating a
+    # real DRT, and as such, it needs access to a real SystemHost, not a MockSystemHost.
+    sys.exit(main(sys.argv[1:], SystemHost(), sys.stdin, sys.stdout, sys.stderr))
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/mock_drt_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/mock_drt_unittest.py
new file mode 100755
index 0000000..1ac051a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/mock_drt_unittest.py
@@ -0,0 +1,268 @@
+#!/usr/bin/env python
+# Copyright (C) 2011 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.
+
+"""Unit tests for MockDRT."""
+
+import sys
+import unittest
+
+from webkitpy.common import newstringio
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+from webkitpy.layout_tests.port import mock_drt
+from webkitpy.layout_tests.port import port_testcase
+from webkitpy.layout_tests.port import test
+from webkitpy.layout_tests.port.factory import PortFactory
+from webkitpy.tool import mocktool
+
+
+mock_options = mocktool.MockOptions(configuration='Release')
+
+
+class MockDRTPortTest(port_testcase.PortTestCase):
+
+    def make_port(self, options=mock_options):
+        host = MockSystemHost()
+        test.add_unit_tests_to_mock_filesystem(host.filesystem)
+        return mock_drt.MockDRTPort(host, port_name='mock-mac-lion', options=options)
+
+    def test_port_name_in_constructor(self):
+        self.assertTrue(mock_drt.MockDRTPort(MockSystemHost(), port_name='mock-test'))
+
+    def test_check_sys_deps(self):
+        pass
+
+    def test_diff_image(self):
+        pass
+
+    def test_diff_image_crashed(self):
+        pass
+
+    def test_uses_apache(self):
+        pass
+
+    def integration_test_http_lock(self):
+        pass
+
+    def integration_test_start_helper(self):
+        pass
+
+    def integration_test_http_server__normal(self):
+        pass
+
+    def integration_test_http_server__fails(self):
+        pass
+
+    def integration_test_websocket_server__normal(self):
+        pass
+
+    def integration_test_websocket_server__fails(self):
+        pass
+
+    def integration_test_helper(self):
+        pass
+
+    def test_get_crash_log(self):
+        pass
+
+    def test_check_build(self):
+        pass
+
+
+class MockDRTTest(unittest.TestCase):
+    def input_line(self, port, test_name, checksum=None):
+        url = port.create_driver(0).test_to_uri(test_name)
+        if url.startswith('file://'):
+            url = url[len('file://'):]
+
+        if checksum:
+            return url + "'" + checksum + '\n'
+        return url + '\n'
+
+    def extra_args(self, pixel_tests):
+        if pixel_tests:
+            return ['--pixel-tests', '-']
+        return ['-']
+
+    def make_drt(self, options, args, host, stdin, stdout, stderr):
+        return mock_drt.MockDRT(options, args, host, stdin, stdout, stderr)
+
+    def make_input_output(self, port, test_name, pixel_tests,
+                          expected_checksum, drt_output, drt_input=None, expected_text=None):
+        if pixel_tests:
+            if not expected_checksum:
+                expected_checksum = port.expected_checksum(test_name)
+        if not drt_input:
+            drt_input = self.input_line(port, test_name, expected_checksum)
+        text_output = expected_text or port.expected_text(test_name) or ''
+
+        if not drt_output:
+            drt_output = self.expected_output(port, test_name, pixel_tests,
+                                              text_output, expected_checksum)
+        return (drt_input, drt_output)
+
+    def expected_output(self, port, test_name, pixel_tests, text_output, expected_checksum):
+        output = ['Content-Type: text/plain\n']
+        if text_output:
+            output.append(text_output)
+        output.append('#EOF\n')
+        if pixel_tests and expected_checksum:
+            output.extend(['\n',
+                           'ActualHash: %s\n' % expected_checksum,
+                           'ExpectedHash: %s\n' % expected_checksum])
+        output.append('#EOF\n')
+        return output
+
+    def assertTest(self, test_name, pixel_tests, expected_checksum=None, drt_output=None, host=None, expected_text=None):
+        port_name = 'test'
+        host = host or MockSystemHost()
+        test.add_unit_tests_to_mock_filesystem(host.filesystem)
+        port = PortFactory(host).get(port_name)
+        drt_input, drt_output = self.make_input_output(port, test_name,
+            pixel_tests, expected_checksum, drt_output, drt_input=None, expected_text=expected_text)
+
+        args = ['--platform', port_name] + self.extra_args(pixel_tests)
+        stdin = newstringio.StringIO(drt_input)
+        stdout = newstringio.StringIO()
+        stderr = newstringio.StringIO()
+        options, args = mock_drt.parse_options(args)
+
+        drt = self.make_drt(options, args, host, stdin, stdout, stderr)
+        res = drt.run()
+
+        self.assertEqual(res, 0)
+
+        # We use the StringIO.buflist here instead of getvalue() because
+        # the StringIO might be a mix of unicode/ascii and 8-bit strings.
+        self.assertEqual(stdout.buflist, drt_output)
+        self.assertEqual(stderr.getvalue(), '' if options.test_shell else '#EOF\n')
+
+    def test_main(self):
+        host = MockSystemHost()
+        test.add_unit_tests_to_mock_filesystem(host.filesystem)
+        stdin = newstringio.StringIO()
+        stdout = newstringio.StringIO()
+        stderr = newstringio.StringIO()
+        res = mock_drt.main(['--platform', 'test'] + self.extra_args(False),
+                            host, stdin, stdout, stderr)
+        self.assertEqual(res, 0)
+        self.assertEqual(stdout.getvalue(), '')
+        self.assertEqual(stderr.getvalue(), '')
+        self.assertEqual(host.filesystem.written_files, {})
+
+    def test_pixeltest_passes(self):
+        # This also tests that we handle HTTP: test URLs properly.
+        self.assertTest('http/tests/passes/text.html', True)
+
+    def test_pixeltest__fails(self):
+        self.assertTest('failures/expected/image_checksum.html', pixel_tests=True,
+            expected_checksum='image_checksum-checksum',
+            drt_output=['Content-Type: text/plain\n',
+                        'image_checksum-txt',
+                        '#EOF\n',
+                        '\n',
+                        'ActualHash: image_checksum-checksum\n',
+                        'ExpectedHash: image_checksum-checksum\n',
+                        '#EOF\n'])
+
+    def test_textonly(self):
+        self.assertTest('passes/image.html', False)
+
+    def test_checksum_in_png(self):
+        self.assertTest('passes/checksum_in_image.html', True)
+
+    def test_missing_image(self):
+        self.assertTest('failures/expected/missing_image.html', True)
+
+    def test_missing_text(self):
+        self.assertTest('failures/expected/missing_text.html', True)
+
+    def test_reftest_match(self):
+        self.assertTest('passes/reftest.html', False, expected_checksum='mock-checksum', expected_text='reference text\n')
+        self.assertTest('passes/reftest.html', True, expected_checksum='mock-checksum', expected_text='reference text\n')
+
+    def test_reftest_mismatch(self):
+        self.assertTest('passes/mismatch.html', False, expected_checksum='mock-checksum', expected_text='reference text\n')
+        self.assertTest('passes/mismatch.html', True, expected_checksum='mock-checksum', expected_text='reference text\n')
+
+
+class MockTestShellTest(MockDRTTest):
+    def extra_args(self, pixel_tests):
+        if pixel_tests:
+            return ['--pixel-tests=/tmp/png_result0.png']
+        return []
+
+    def make_drt(self, options, args, host, stdin, stdout, stderr):
+        options.test_shell = True
+
+        # We have to set these by hand because --platform test won't trigger
+        # the Chromium code paths.
+        options.pixel_path = '/tmp/png_result0.png'
+        options.pixel_tests = True
+
+        return mock_drt.MockTestShell(options, args, host, stdin, stdout, stderr)
+
+    def input_line(self, port, test_name, checksum=None):
+        url = port.create_driver(0).test_to_uri(test_name)
+        if checksum:
+            return url + ' 6000 ' + checksum + '\n'
+        return url + ' 6000\n'
+
+    def expected_output(self, port, test_name, pixel_tests, text_output, expected_checksum):
+        url = port.create_driver(0).test_to_uri(test_name)
+        output = ['#URL:%s\n' % url]
+        if expected_checksum:
+            output.append('#MD5:%s\n' % expected_checksum)
+        if text_output:
+            output.append(text_output)
+            if not text_output.endswith('\n'):
+                output.append('\n')
+        output.append('#EOF\n')
+        return output
+
+    def test_pixeltest__fails(self):
+        host = MockSystemHost()
+        url = '#URL:file://'
+        url = url + '%s/failures/expected/image_checksum.html' % PortFactory(host).get('test').layout_tests_dir()
+        self.assertTest('failures/expected/image_checksum.html', pixel_tests=True,
+            expected_checksum='image_checksum',
+            drt_output=[url + '\n',
+                        '#MD5:image_checksum-checksum\n',
+                        'image_checksum-txt',
+                        '\n',
+                        '#EOF\n'],
+            host=host)
+        self.assertEquals(host.filesystem.written_files,
+            {'/tmp/png_result0.png': 'image_checksum\x8a-pngtEXtchecksum\x00image_checksum-checksum'})
+
+    def test_test_shell_parse_options(self):
+        options, args = mock_drt.parse_options(['--platform', 'chromium-mac', '--test-shell',
+            '--pixel-tests=/tmp/png_result0.png'])
+        self.assertTrue(options.test_shell)
+        self.assertTrue(options.pixel_tests)
+        self.assertEquals(options.pixel_path, '/tmp/png_result0.png')
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/port_testcase.py b/Tools/Scripts/webkitpy/layout_tests/port/port_testcase.py
new file mode 100755
index 0000000..b036f4b
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/port_testcase.py
@@ -0,0 +1,649 @@
+# 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.
+
+"""Unit testing base class for Port implementations."""
+
+import errno
+import logging
+import os
+import socket
+import sys
+import time
+import unittest
+
+from webkitpy.common.system.executive_mock import MockExecutive
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+from webkitpy.layout_tests.port.base import Port
+from webkitpy.layout_tests.port.config_mock import MockConfig
+from webkitpy.layout_tests.port.server_process_mock import MockServerProcess
+from webkitpy.layout_tests.servers import http_server_base
+from webkitpy.tool.mocktool import MockOptions
+
+
+# FIXME: get rid of this fixture
+class TestWebKitPort(Port):
+    port_name = "testwebkitport"
+
+    def __init__(self, symbols_string=None,
+                 expectations_file=None, skips_file=None, host=None, config=None,
+                 **kwargs):
+        self.symbols_string = symbols_string  # Passing "" disables all staticly-detectable features.
+        host = host or MockSystemHost()
+        config = config or MockConfig()
+        super(TestWebKitPort, self).__init__(host=host, config=config, **kwargs)
+
+    def all_test_configurations(self):
+        return [self.test_configuration()]
+
+    def _symbols_string(self):
+        return self.symbols_string
+
+    def _tests_for_other_platforms(self):
+        return ["media", ]
+
+    def _tests_for_disabled_features(self):
+        return ["accessibility", ]
+
+
+class PortTestCase(unittest.TestCase):
+    """Tests that all Port implementations must pass."""
+    HTTP_PORTS = (8000, 8080, 8443)
+    WEBSOCKET_PORTS = (8880,)
+
+    # Subclasses override this to point to their Port subclass.
+    os_name = None
+    os_version = None
+    port_maker = TestWebKitPort
+
+    def make_port(self, host=None, port_name=None, options=None, os_name=None, os_version=None, config=None, **kwargs):
+        host = host or MockSystemHost(os_name=(os_name or self.os_name), os_version=(os_version or self.os_version))
+        options = options or MockOptions(configuration='Release')
+        config = config or MockConfig(filesystem=host.filesystem, default_configuration='Release')
+        port_name = port_name or self.port_name
+        port_name = self.port_maker.determine_full_port_name(host, options, port_name)
+        return self.port_maker(host, port_name, options=options, config=config, **kwargs)
+
+    def test_default_max_locked_shards(self):
+        port = self.make_port()
+        port.default_child_processes = lambda: 16
+        self.assertEquals(port.default_max_locked_shards(), 1)
+        port.default_child_processes = lambda: 2
+        self.assertEquals(port.default_max_locked_shards(), 1)
+
+    def test_default_timeout_ms(self):
+        self.assertEquals(self.make_port(options=MockOptions(configuration='Release')).default_timeout_ms(), 35000)
+        self.assertEquals(self.make_port(options=MockOptions(configuration='Debug')).default_timeout_ms(), 35000)
+
+    def test_default_pixel_tests(self):
+        self.assertEquals(self.make_port().default_pixel_tests(), False)
+
+    def test_driver_cmd_line(self):
+        port = self.make_port()
+        self.assertTrue(len(port.driver_cmd_line()))
+
+        options = MockOptions(additional_drt_flag=['--foo=bar', '--foo=baz'])
+        port = self.make_port(options=options)
+        cmd_line = port.driver_cmd_line()
+        self.assertTrue('--foo=bar' in cmd_line)
+        self.assertTrue('--foo=baz' in cmd_line)
+
+    def test_expectations_files(self):
+        self.assertNotEquals(self.make_port().expectations_files(), [])
+
+    def test_uses_apache(self):
+        self.assertTrue(self.make_port()._uses_apache())
+
+    def assert_servers_are_down(self, host, ports):
+        for port in ports:
+            try:
+                test_socket = socket.socket()
+                test_socket.connect((host, port))
+                self.fail()
+            except IOError, e:
+                self.assertTrue(e.errno in (errno.ECONNREFUSED, errno.ECONNRESET))
+            finally:
+                test_socket.close()
+
+    def assert_servers_are_up(self, host, ports):
+        for port in ports:
+            try:
+                test_socket = socket.socket()
+                test_socket.connect((host, port))
+            except IOError, e:
+                self.fail('failed to connect to %s:%d' % (host, port))
+            finally:
+                test_socket.close()
+
+    def integration_test_http_lock(self):
+        port = self.make_port()
+        # Only checking that no exception is raised.
+        port.acquire_http_lock()
+        port.release_http_lock()
+
+    def integration_test_check_sys_deps(self):
+        port = self.make_port()
+        # Only checking that no exception is raised.
+        port.check_sys_deps(True)
+
+    def integration_test_helper(self):
+        port = self.make_port()
+        # Only checking that no exception is raised.
+        port.start_helper()
+        port.stop_helper()
+
+    def integration_test_http_server__normal(self):
+        port = self.make_port()
+        self.assert_servers_are_down('localhost', self.HTTP_PORTS)
+        port.start_http_server()
+        self.assert_servers_are_up('localhost', self.HTTP_PORTS)
+        port.stop_http_server()
+        self.assert_servers_are_down('localhost', self.HTTP_PORTS)
+
+    def integration_test_http_server__fails(self):
+        port = self.make_port()
+        # Test that if a port isn't available, the call fails.
+        for port_number in self.HTTP_PORTS:
+            test_socket = socket.socket()
+            try:
+                try:
+                    test_socket.bind(('localhost', port_number))
+                except socket.error, e:
+                    if e.errno in (errno.EADDRINUSE, errno.EALREADY):
+                        self.fail('could not bind to port %d' % port_number)
+                    raise
+                try:
+                    port.start_http_server()
+                    self.fail('should not have been able to start the server while bound to %d' % port_number)
+                except http_server_base.ServerError, e:
+                    pass
+            finally:
+                port.stop_http_server()
+                test_socket.close()
+
+        # Test that calling start() twice fails.
+        try:
+            port.start_http_server()
+            self.assertRaises(AssertionError, port.start_http_server)
+        finally:
+            port.stop_http_server()
+
+    def integration_test_http_server__two_servers(self):
+        # Test that calling start() on two different ports causes the
+        # first port to be treated as stale and killed.
+        port = self.make_port()
+        # Test that if a port isn't available, the call fails.
+        port.start_http_server()
+        new_port = self.make_port()
+        try:
+            new_port.start_http_server()
+
+            # Check that the first server was killed.
+            self.assertFalse(port._executive.check_running_pid(port._http_server._pid))
+
+            # Check that there is something running.
+            self.assert_servers_are_up('localhost', self.HTTP_PORTS)
+
+            # Test that calling stop() on a killed server is harmless.
+            port.stop_http_server()
+        finally:
+            port.stop_http_server()
+            new_port.stop_http_server()
+
+            # Test that calling stop() twice is harmless.
+            new_port.stop_http_server()
+
+    def integration_test_image_diff(self):
+        port = self.make_port()
+        # FIXME: This test will never run since we are using a MockFilesystem for these tests!?!?
+        if not port.check_image_diff():
+            # The port hasn't been built - don't run the tests.
+            return
+
+        dir = port.layout_tests_dir()
+        file1 = port._filesystem.join(dir, 'fast', 'css', 'button_center.png')
+        contents1 = port._filesystem.read_binary_file(file1)
+        file2 = port._filesystem.join(dir, 'fast', 'css',
+                                      'remove-shorthand-expected.png')
+        contents2 = port._filesystem.read_binary_file(file2)
+        tmpfd, tmpfile = port._filesystem.open_binary_tempfile('')
+        tmpfd.close()
+
+        self.assertFalse(port.diff_image(contents1, contents1)[0])
+        self.assertTrue(port.diff_image(contents1, contents2)[0])
+
+        self.assertTrue(port.diff_image(contents1, contents2, tmpfile)[0])
+
+        port._filesystem.remove(tmpfile)
+
+    def test_diff_image__missing_both(self):
+        port = self.make_port()
+        self.assertFalse(port.diff_image(None, None)[0])
+        self.assertFalse(port.diff_image(None, '')[0])
+        self.assertFalse(port.diff_image('', None)[0])
+
+        self.assertFalse(port.diff_image('', '')[0])
+
+    def test_diff_image__missing_actual(self):
+        port = self.make_port()
+        self.assertTrue(port.diff_image(None, 'foo')[0])
+        self.assertTrue(port.diff_image('', 'foo')[0])
+
+    def test_diff_image__missing_expected(self):
+        port = self.make_port()
+        self.assertTrue(port.diff_image('foo', None)[0])
+        self.assertTrue(port.diff_image('foo', '')[0])
+
+    def test_diff_image(self):
+        port = self.make_port()
+        self.proc = None
+
+        def make_proc(port, nm, cmd, env):
+            self.proc = MockServerProcess(port, nm, cmd, env, lines=['diff: 100% failed\n', 'diff: 100% failed\n'])
+            return self.proc
+
+        port._server_process_constructor = make_proc
+        port.setup_test_run()
+        self.assertEquals(port.diff_image('foo', 'bar'), ('', 100.0, None))
+        self.assertEquals(self.proc.cmd[1:3], ["--tolerance", "0.1"])
+
+        self.assertEquals(port.diff_image('foo', 'bar', None), ('', 100.0, None))
+        self.assertEquals(self.proc.cmd[1:3], ["--tolerance", "0.1"])
+
+        self.assertEquals(port.diff_image('foo', 'bar', 0), ('', 100.0, None))
+        self.assertEquals(self.proc.cmd[1:3], ["--tolerance", "0"])
+
+        port.clean_up_test_run()
+        self.assertTrue(self.proc.stopped)
+        self.assertEquals(port._image_differ, None)
+
+    def test_diff_image_crashed(self):
+        port = self.make_port()
+        self.proc = None
+
+        def make_proc(port, nm, cmd, env):
+            self.proc = MockServerProcess(port, nm, cmd, env, crashed=True)
+            return self.proc
+
+        port._server_process_constructor = make_proc
+        port.setup_test_run()
+        self.assertEquals(port.diff_image('foo', 'bar'), ('', 0, 'ImageDiff crashed\n'))
+        port.clean_up_test_run()
+
+    def test_check_wdiff(self):
+        port = self.make_port()
+        port.check_wdiff()
+
+    def integration_test_websocket_server__normal(self):
+        port = self.make_port()
+        self.assert_servers_are_down('localhost', self.WEBSOCKET_PORTS)
+        port.start_websocket_server()
+        self.assert_servers_are_up('localhost', self.WEBSOCKET_PORTS)
+        port.stop_websocket_server()
+        self.assert_servers_are_down('localhost', self.WEBSOCKET_PORTS)
+
+    def integration_test_websocket_server__fails(self):
+        port = self.make_port()
+
+        # Test that start() fails if a port isn't available.
+        for port_number in self.WEBSOCKET_PORTS:
+            test_socket = socket.socket()
+            try:
+                test_socket.bind(('localhost', port_number))
+                try:
+                    port.start_websocket_server()
+                    self.fail('should not have been able to start the server while bound to %d' % port_number)
+                except http_server_base.ServerError, e:
+                    pass
+            finally:
+                port.stop_websocket_server()
+                test_socket.close()
+
+        # Test that calling start() twice fails.
+        try:
+            port.start_websocket_server()
+            self.assertRaises(AssertionError, port.start_websocket_server)
+        finally:
+            port.stop_websocket_server()
+
+    def integration_test_websocket_server__two_servers(self):
+        port = self.make_port()
+
+        # Test that calling start() on two different ports causes the
+        # first port to be treated as stale and killed.
+        port.start_websocket_server()
+        new_port = self.make_port()
+        try:
+            new_port.start_websocket_server()
+
+            # Check that the first server was killed.
+            self.assertFalse(port._executive.check_running_pid(port._websocket_server._pid))
+
+            # Check that there is something running.
+            self.assert_servers_are_up('localhost', self.WEBSOCKET_PORTS)
+
+            # Test that calling stop() on a killed server is harmless.
+            port.stop_websocket_server()
+        finally:
+            port.stop_websocket_server()
+            new_port.stop_websocket_server()
+
+            # Test that calling stop() twice is harmless.
+            new_port.stop_websocket_server()
+
+    def test_test_configuration(self):
+        port = self.make_port()
+        self.assertTrue(port.test_configuration())
+
+    def test_all_test_configurations(self):
+        port = self.make_port()
+        self.assertTrue(len(port.all_test_configurations()) > 0)
+        self.assertTrue(port.test_configuration() in port.all_test_configurations(), "%s not in %s" % (port.test_configuration(), port.all_test_configurations()))
+
+    def integration_test_http_server__loop(self):
+        port = self.make_port()
+
+        i = 0
+        while i < 10:
+            self.assert_servers_are_down('localhost', self.HTTP_PORTS)
+            port.start_http_server()
+
+            # We sleep in between alternating runs to ensure that this
+            # test handles both back-to-back starts and stops and
+            # starts and stops separated by a delay.
+            if i % 2:
+                time.sleep(0.1)
+
+            self.assert_servers_are_up('localhost', self.HTTP_PORTS)
+            port.stop_http_server()
+            if i % 2:
+                time.sleep(0.1)
+
+            i += 1
+
+    def test_get_crash_log(self):
+        port = self.make_port()
+        self.assertEquals(port._get_crash_log(None, None, None, None, newer_than=None),
+           (None,
+            'crash log for <unknown process name> (pid <unknown>):\n'
+            'STDOUT: <empty>\n'
+            'STDERR: <empty>\n'))
+
+        self.assertEquals(port._get_crash_log('foo', 1234, 'out bar\nout baz', 'err bar\nerr baz\n', newer_than=None),
+            ('err bar\nerr baz\n',
+             'crash log for foo (pid 1234):\n'
+             'STDOUT: out bar\n'
+             'STDOUT: out baz\n'
+             'STDERR: err bar\n'
+             'STDERR: err baz\n'))
+
+        self.assertEquals(port._get_crash_log('foo', 1234, 'foo\xa6bar', 'foo\xa6bar', newer_than=None),
+            ('foo\xa6bar',
+             u'crash log for foo (pid 1234):\n'
+             u'STDOUT: foo\ufffdbar\n'
+             u'STDERR: foo\ufffdbar\n'))
+
+        self.assertEquals(port._get_crash_log('foo', 1234, 'foo\xa6bar', 'foo\xa6bar', newer_than=1.0),
+            ('foo\xa6bar',
+             u'crash log for foo (pid 1234):\n'
+             u'STDOUT: foo\ufffdbar\n'
+             u'STDERR: foo\ufffdbar\n'))
+
+    def assert_build_path(self, options, dirs, expected_path):
+        port = self.make_port(options=options)
+        for directory in dirs:
+            port.host.filesystem.maybe_make_directory(directory)
+        self.assertEquals(port._build_path(), expected_path)
+
+    def test_expectations_ordering(self):
+        port = self.make_port()
+        for path in port.expectations_files():
+            port._filesystem.write_text_file(path, '')
+        ordered_dict = port.expectations_dict()
+        self.assertEquals(port.path_to_test_expectations_file(), ordered_dict.keys()[0])
+
+        options = MockOptions(additional_expectations=['/tmp/foo', '/tmp/bar'])
+        port = self.make_port(options=options)
+        for path in port.expectations_files():
+            port._filesystem.write_text_file(path, '')
+        port._filesystem.write_text_file('/tmp/foo', 'foo')
+        port._filesystem.write_text_file('/tmp/bar', 'bar')
+        ordered_dict = port.expectations_dict()
+        self.assertEquals(ordered_dict.keys()[-2:], options.additional_expectations)
+        self.assertEquals(ordered_dict.values()[-2:], ['foo', 'bar'])
+
+    def test_path_to_test_expectations_file(self):
+        port = TestWebKitPort()
+        port._options = MockOptions(webkit_test_runner=False)
+        self.assertEqual(port.path_to_test_expectations_file(), '/mock-checkout/LayoutTests/platform/testwebkitport/TestExpectations')
+
+        port = TestWebKitPort()
+        port._options = MockOptions(webkit_test_runner=True)
+        self.assertEqual(port.path_to_test_expectations_file(), '/mock-checkout/LayoutTests/platform/testwebkitport/TestExpectations')
+
+        port = TestWebKitPort()
+        port.host.filesystem.files['/mock-checkout/LayoutTests/platform/testwebkitport/TestExpectations'] = 'some content'
+        port._options = MockOptions(webkit_test_runner=False)
+        self.assertEqual(port.path_to_test_expectations_file(), '/mock-checkout/LayoutTests/platform/testwebkitport/TestExpectations')
+
+    def test_skipped_directories_for_symbols(self):
+        # This first test confirms that the commonly found symbols result in the expected skipped directories.
+        symbols_string = " ".join(["GraphicsLayer", "WebCoreHas3DRendering", "isXHTMLMPDocument", "fooSymbol"])
+        expected_directories = set([
+            "mathml",  # Requires MathMLElement
+            "fast/canvas/webgl",  # Requires WebGLShader
+            "compositing/webgl",  # Requires WebGLShader
+            "http/tests/canvas/webgl",  # Requires WebGLShader
+            "mhtml",  # Requires MHTMLArchive
+            "fast/css/variables",  # Requires CSS Variables
+            "inspector/styles/variables",  # Requires CSS Variables
+        ])
+
+        result_directories = set(TestWebKitPort(symbols_string, None)._skipped_tests_for_unsupported_features(test_list=['mathml/foo.html']))
+        self.assertEqual(result_directories, expected_directories)
+
+        # Test that the nm string parsing actually works:
+        symbols_string = """
+000000000124f498 s __ZZN7WebCore13GraphicsLayer12replaceChildEPS0_S1_E19__PRETTY_FUNCTION__
+000000000124f500 s __ZZN7WebCore13GraphicsLayer13addChildAboveEPS0_S1_E19__PRETTY_FUNCTION__
+000000000124f670 s __ZZN7WebCore13GraphicsLayer13addChildBelowEPS0_S1_E19__PRETTY_FUNCTION__
+"""
+        # Note 'compositing' is not in the list of skipped directories (hence the parsing of GraphicsLayer worked):
+        expected_directories = set(['mathml', 'transforms/3d', 'compositing/webgl', 'fast/canvas/webgl', 'animations/3d', 'mhtml', 'http/tests/canvas/webgl', 'fast/css/variables', 'inspector/styles/variables'])
+        result_directories = set(TestWebKitPort(symbols_string, None)._skipped_tests_for_unsupported_features(test_list=['mathml/foo.html']))
+        self.assertEqual(result_directories, expected_directories)
+
+    def test_skipped_directories_for_features(self):
+        supported_features = ["Accelerated Compositing", "Foo Feature"]
+        expected_directories = set(["animations/3d", "transforms/3d"])
+        port = TestWebKitPort(None, supported_features)
+        port._runtime_feature_list = lambda: supported_features
+        result_directories = set(port._skipped_tests_for_unsupported_features(test_list=["animations/3d/foo.html"]))
+        self.assertEqual(result_directories, expected_directories)
+
+    def test_skipped_directories_for_features_no_matching_tests_in_test_list(self):
+        supported_features = ["Accelerated Compositing", "Foo Feature"]
+        expected_directories = set([])
+        result_directories = set(TestWebKitPort(None, supported_features)._skipped_tests_for_unsupported_features(test_list=['foo.html']))
+        self.assertEqual(result_directories, expected_directories)
+
+    def test_skipped_tests_for_unsupported_features_empty_test_list(self):
+        supported_features = ["Accelerated Compositing", "Foo Feature"]
+        expected_directories = set([])
+        result_directories = set(TestWebKitPort(None, supported_features)._skipped_tests_for_unsupported_features(test_list=None))
+        self.assertEqual(result_directories, expected_directories)
+
+    def test_skipped_layout_tests(self):
+        self.assertEqual(TestWebKitPort(None, None).skipped_layout_tests(test_list=[]), set(['media']))
+
+    def test_expectations_files(self):
+        port = TestWebKitPort()
+
+        def platform_dirs(port):
+            return [port.host.filesystem.basename(port.host.filesystem.dirname(f)) for f in port.expectations_files()]
+
+        self.assertEqual(platform_dirs(port), ['testwebkitport'])
+
+        port._name = "testwebkitport-version"
+        self.assertEqual(platform_dirs(port), ['testwebkitport', 'testwebkitport-version'])
+
+        port._options = MockOptions(webkit_test_runner=True)
+        self.assertEqual(platform_dirs(port), ['testwebkitport', 'testwebkitport-version', 'testwebkitport-wk2', 'wk2'])
+
+        port._options = MockOptions(additional_platform_directory=["internal-testwebkitport"])
+        self.assertEqual(platform_dirs(port), ['testwebkitport', 'testwebkitport-version', 'internal-testwebkitport'])
+
+    def test_root_option(self):
+        port = TestWebKitPort()
+        port._options = MockOptions(root='/foo')
+        self.assertEqual(port._path_to_driver(), "/foo/DumpRenderTree")
+
+    def test_test_expectations(self):
+        # Check that we read the expectations file
+        host = MockSystemHost()
+        host.filesystem.write_text_file('/mock-checkout/LayoutTests/platform/testwebkitport/TestExpectations',
+            'BUG_TESTEXPECTATIONS SKIP : fast/html/article-element.html = FAIL\n')
+        port = TestWebKitPort(host=host)
+        self.assertEqual(''.join(port.expectations_dict().values()), 'BUG_TESTEXPECTATIONS SKIP : fast/html/article-element.html = FAIL\n')
+
+    def test_build_driver(self):
+        output = OutputCapture()
+        port = TestWebKitPort()
+        # Delay setting _executive to avoid logging during construction
+        port._executive = MockExecutive(should_log=True)
+        port._options = MockOptions(configuration="Release")  # This should not be necessary, but I think TestWebKitPort is actually reading from disk (and thus detects the current configuration).
+        expected_stderr = "MOCK run_command: ['Tools/Scripts/build-dumprendertree', '--release'], cwd=/mock-checkout, env={'LC_ALL': 'C', 'MOCK_ENVIRON_COPY': '1'}\n"
+        self.assertTrue(output.assert_outputs(self, port._build_driver, expected_stderr=expected_stderr, expected_logs=''))
+
+        # Make sure when passed --webkit-test-runner we build the right tool.
+        port._options = MockOptions(webkit_test_runner=True, configuration="Release")
+        expected_stderr = "MOCK run_command: ['Tools/Scripts/build-dumprendertree', '--release'], cwd=/mock-checkout, env={'LC_ALL': 'C', 'MOCK_ENVIRON_COPY': '1'}\nMOCK run_command: ['Tools/Scripts/build-webkittestrunner', '--release'], cwd=/mock-checkout, env={'LC_ALL': 'C', 'MOCK_ENVIRON_COPY': '1'}\n"
+        self.assertTrue(output.assert_outputs(self, port._build_driver, expected_stderr=expected_stderr, expected_logs=''))
+
+        # Make sure we show the build log when --verbose is passed, which we simulate by setting the logging level to DEBUG.
+        output.set_log_level(logging.DEBUG)
+        port._options = MockOptions(configuration="Release")
+        expected_stderr = "MOCK run_command: ['Tools/Scripts/build-dumprendertree', '--release'], cwd=/mock-checkout, env={'LC_ALL': 'C', 'MOCK_ENVIRON_COPY': '1'}\n"
+        expected_logs = "Output of ['Tools/Scripts/build-dumprendertree', '--release']:\nMOCK output of child process\n"
+        self.assertTrue(output.assert_outputs(self, port._build_driver, expected_stderr=expected_stderr, expected_logs=expected_logs))
+        output.set_log_level(logging.INFO)
+
+        # Make sure that failure to build returns False.
+        port._executive = MockExecutive(should_log=True, should_throw=True)
+        # Because WK2 currently has to build both webkittestrunner and DRT, if DRT fails, that's the only one it tries.
+        expected_stderr = "MOCK run_command: ['Tools/Scripts/build-dumprendertree', '--release'], cwd=/mock-checkout, env={'LC_ALL': 'C', 'MOCK_ENVIRON_COPY': '1'}\n"
+        expected_logs = "MOCK ScriptError\n\nMOCK output of child process\n"
+        self.assertFalse(output.assert_outputs(self, port._build_driver, expected_stderr=expected_stderr, expected_logs=expected_logs))
+
+    def _assert_config_file_for_platform(self, port, platform, config_file):
+        self.assertEquals(port._apache_config_file_name_for_platform(platform), config_file)
+
+    def test_linux_distro_detection(self):
+        port = TestWebKitPort()
+        self.assertFalse(port._is_redhat_based())
+        self.assertFalse(port._is_debian_based())
+
+        port._filesystem = MockFileSystem({'/etc/redhat-release': ''})
+        self.assertTrue(port._is_redhat_based())
+        self.assertFalse(port._is_debian_based())
+
+        port._filesystem = MockFileSystem({'/etc/debian_version': ''})
+        self.assertFalse(port._is_redhat_based())
+        self.assertTrue(port._is_debian_based())
+
+    def test_apache_config_file_name_for_platform(self):
+        port = TestWebKitPort()
+        self._assert_config_file_for_platform(port, 'cygwin', 'cygwin-httpd.conf')
+
+        self._assert_config_file_for_platform(port, 'linux2', 'apache2-httpd.conf')
+        self._assert_config_file_for_platform(port, 'linux3', 'apache2-httpd.conf')
+
+        port._is_redhat_based = lambda: True
+        self._assert_config_file_for_platform(port, 'linux2', 'fedora-httpd.conf')
+
+        port = TestWebKitPort()
+        port._is_debian_based = lambda: True
+        self._assert_config_file_for_platform(port, 'linux2', 'apache2-debian-httpd.conf')
+
+        self._assert_config_file_for_platform(port, 'mac', 'apache2-httpd.conf')
+        self._assert_config_file_for_platform(port, 'win32', 'apache2-httpd.conf')  # win32 isn't a supported sys.platform.  AppleWin/WinCairo/WinCE ports all use cygwin.
+        self._assert_config_file_for_platform(port, 'barf', 'apache2-httpd.conf')
+
+    def test_path_to_apache_config_file(self):
+        port = TestWebKitPort()
+
+        saved_environ = os.environ.copy()
+        try:
+            os.environ['WEBKIT_HTTP_SERVER_CONF_PATH'] = '/path/to/httpd.conf'
+            self.assertRaises(IOError, port._path_to_apache_config_file)
+            port._filesystem.write_text_file('/existing/httpd.conf', 'Hello, world!')
+            os.environ['WEBKIT_HTTP_SERVER_CONF_PATH'] = '/existing/httpd.conf'
+            self.assertEquals(port._path_to_apache_config_file(), '/existing/httpd.conf')
+        finally:
+            os.environ = saved_environ.copy()
+
+        # Mock out _apache_config_file_name_for_platform to ignore the passed sys.platform value.
+        port._apache_config_file_name_for_platform = lambda platform: 'httpd.conf'
+        self.assertEquals(port._path_to_apache_config_file(), '/mock-checkout/LayoutTests/http/conf/httpd.conf')
+
+        # Check that even if we mock out _apache_config_file_name, the environment variable takes precedence.
+        saved_environ = os.environ.copy()
+        try:
+            os.environ['WEBKIT_HTTP_SERVER_CONF_PATH'] = '/existing/httpd.conf'
+            self.assertEquals(port._path_to_apache_config_file(), '/existing/httpd.conf')
+        finally:
+            os.environ = saved_environ.copy()
+
+    def test_check_build(self):
+        port = self.make_port(options=MockOptions(build=True))
+        self.build_called = False
+
+        def build_driver_called():
+            self.build_called = True
+            return True
+
+        port._build_driver = build_driver_called
+        port.check_build(False)
+        self.assertTrue(self.build_called)
+
+        port = self.make_port(options=MockOptions(root='/tmp', build=True))
+        self.build_called = False
+        port._build_driver = build_driver_called
+        port.check_build(False)
+        self.assertFalse(self.build_called, None)
+
+        port = self.make_port(options=MockOptions(build=False))
+        self.build_called = False
+        port._build_driver = build_driver_called
+        port.check_build(False)
+        self.assertFalse(self.build_called, None)
+
+    def test_additional_platform_directory(self):
+        port = self.make_port(options=MockOptions(additional_platform_directory=['/tmp/foo']))
+        self.assertEquals(port.baseline_search_path()[0], '/tmp/foo')
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/pulseaudio_sanitizer.py b/Tools/Scripts/webkitpy/layout_tests/port/pulseaudio_sanitizer.py
new file mode 100644
index 0000000..f4574a9
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/pulseaudio_sanitizer.py
@@ -0,0 +1,85 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# Copyrigth (C) 2012 Intel Corporation
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import logging
+import os
+import subprocess
+
+
+_log = logging.getLogger(__name__)
+
+
+# Shared by GTK and EFL for pulseaudio sanitizing before running tests.
+class PulseAudioSanitizer:
+    def _unload_pulseaudio_module(self):
+        # Unload pulseaudio's module-stream-restore, since it remembers
+        # volume settings from different runs, and could affect
+        # multimedia tests results
+        self._pa_module_index = -1
+        with open(os.devnull, 'w') as devnull:
+            try:
+                pactl_process = subprocess.Popen(["pactl", "list", "short", "modules"], stdout=subprocess.PIPE, stderr=devnull)
+                pactl_process.wait()
+            except OSError:
+                # pactl might not be available.
+                _log.debug('pactl not found. Please install pulseaudio-utils to avoid some potential media test failures.')
+                return
+        modules_list = pactl_process.communicate()[0]
+        for module in modules_list.splitlines():
+            if module.find("module-stream-restore") >= 0:
+                # Some pulseaudio-utils versions don't provide
+                # the index, just an empty string
+                self._pa_module_index = module.split('\t')[0] or -1
+                try:
+                    # Since they could provide other stuff (not an index
+                    # nor an empty string, let's make sure this is an int.
+                    if int(self._pa_module_index) != -1:
+                        pactl_process = subprocess.Popen(["pactl", "unload-module", self._pa_module_index])
+                        pactl_process.wait()
+                        if pactl_process.returncode == 0:
+                            _log.debug('Unloaded module-stream-restore successfully')
+                        else:
+                            _log.debug('Unloading module-stream-restore failed')
+                except ValueError:
+                        # pactl should have returned an index if the module is found
+                        _log.debug('Unable to parse module index. Please check if your pulseaudio-utils version is too old.')
+                return
+
+    def _restore_pulseaudio_module(self):
+        # If pulseaudio's module-stream-restore was previously unloaded,
+        # restore it back. We shouldn't need extra checks here, since an
+        # index != -1 here means we successfully unloaded it previously.
+        if self._pa_module_index != -1:
+            with open(os.devnull, 'w') as devnull:
+                pactl_process = subprocess.Popen(["pactl", "load-module", "module-stream-restore"], stdout=devnull, stderr=devnull)
+                pactl_process.wait()
+                if pactl_process.returncode == 0:
+                    _log.debug('Restored module-stream-restore successfully')
+                else:
+                    _log.debug('Restoring module-stream-restore failed')
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/qt.py b/Tools/Scripts/webkitpy/layout_tests/port/qt.py
new file mode 100644
index 0000000..55f13ee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/qt.py
@@ -0,0 +1,184 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""QtWebKit implementation of the Port interface."""
+
+import glob
+import logging
+import re
+import sys
+import os
+
+from webkitpy.common.memoized import memoized
+from webkitpy.layout_tests.models.test_configuration import TestConfiguration
+from webkitpy.layout_tests.port.base import Port
+from webkitpy.layout_tests.port.xvfbdriver import XvfbDriver
+
+_log = logging.getLogger(__name__)
+
+
+class QtPort(Port):
+    ALL_VERSIONS = ['linux', 'win', 'mac']
+    port_name = "qt"
+
+    def _wk2_port_name(self):
+        return "qt-5.0-wk2"
+
+    def _port_flag_for_scripts(self):
+        return "--qt"
+
+    @classmethod
+    def determine_full_port_name(cls, host, options, port_name):
+        if port_name and port_name != cls.port_name:
+            return port_name
+        return port_name + '-' + host.platform.os_name
+
+    # sys_platform exists only for unit testing.
+    def __init__(self, host, port_name, **kwargs):
+        super(QtPort, self).__init__(host, port_name, **kwargs)
+
+        # FIXME: This will allow Port.baseline_search_path
+        # to do the right thing, but doesn't include support for qt-4.8 or qt-arm (seen in LayoutTests/platform) yet.
+        self._operating_system = port_name.replace('qt-', '')
+
+        # FIXME: Why is this being set at all?
+        self._version = self.operating_system()
+
+    def _generate_all_test_configurations(self):
+        configurations = []
+        for version in self.ALL_VERSIONS:
+            for build_type in self.ALL_BUILD_TYPES:
+                configurations.append(TestConfiguration(version=version, architecture='x86', build_type=build_type))
+        return configurations
+
+    def _build_driver(self):
+        # The Qt port builds DRT as part of the main build step
+        return True
+
+    def _path_to_driver(self):
+        return self._build_path('bin/%s' % self.driver_name())
+
+    def _path_to_image_diff(self):
+        return self._build_path('bin/ImageDiff')
+
+    def _path_to_webcore_library(self):
+        if self.operating_system() == 'mac':
+            return self._build_path('lib/QtWebKitWidgets.framework/QtWebKitWidgets')
+        else:
+            return self._build_path('lib/libQtWebKitWidgets.so')
+
+    def _modules_to_search_for_symbols(self):
+        # We search in every library to be reliable in the case of building with CONFIG+=force_static_libs_as_shared.
+        if self.operating_system() == 'mac':
+            frameworks = glob.glob(os.path.join(self._build_path('lib'), '*.framework'))
+            return [os.path.join(framework, os.path.splitext(os.path.basename(framework))[0]) for framework in frameworks]
+        else:
+            suffix = 'dll' if self.operating_system() == 'win' else 'so'
+            return glob.glob(os.path.join(self._build_path('lib'), 'lib*.' + suffix))
+
+    @memoized
+    def qt_version(self):
+        version = ''
+        try:
+            for line in self._executive.run_command(['qmake', '-v']).split('\n'):
+                match = re.search('Qt\sversion\s(?P<version>\d\.\d)', line)
+                if match:
+                    version = match.group('version')
+                    break
+        except OSError:
+            version = '4.8'
+        return version
+
+    def _search_paths(self):
+        # qt-5.0-wk1    qt-5.0-wk2
+        #            \/
+        #         qt-5.0    qt-4.8
+        #                \/
+        #    (qt-linux|qt-mac|qt-win)
+        #                |
+        #               qt
+        search_paths = []
+        version = self.qt_version()
+        if '5.0' in version:
+            if self.get_option('webkit_test_runner'):
+                search_paths.append('qt-5.0-wk2')
+            else:
+                search_paths.append('qt-5.0-wk1')
+        if '4.8' in version:
+            search_paths.append('qt-4.8')
+        elif version:
+            search_paths.append('qt-5.0')
+        search_paths.append(self.port_name + '-' + self.operating_system())
+        search_paths.append(self.port_name)
+        return search_paths
+
+    def default_baseline_search_path(self):
+        return map(self._webkit_baseline_path, self._search_paths())
+
+    def expectations_files(self):
+        paths = self._search_paths()
+        if self.get_option('webkit_test_runner'):
+            paths.append('wk2')
+
+        # expectations_files() uses the directories listed in _search_paths reversed.
+        # e.g. qt -> qt-linux -> qt-4.8
+        return list(reversed([self._filesystem.join(self._webkit_baseline_path(p), 'TestExpectations') for p in paths]))
+
+    def setup_environ_for_server(self, server_name=None):
+        clean_env = super(QtPort, self).setup_environ_for_server(server_name)
+        clean_env['QTWEBKIT_PLUGIN_PATH'] = self._build_path('lib/plugins')
+        self._copy_value_from_environ_if_set(clean_env, 'QT_DRT_WEBVIEW_MODE')
+        self._copy_value_from_environ_if_set(clean_env, 'DYLD_IMAGE_SUFFIX')
+        self._copy_value_from_environ_if_set(clean_env, 'QT_WEBKIT_LOG')
+        self._copy_value_from_environ_if_set(clean_env, 'DISABLE_NI_WARNING')
+        self._copy_value_from_environ_if_set(clean_env, 'QT_WEBKIT_PAUSE_UI_PROCESS')
+        self._copy_value_from_environ_if_set(clean_env, 'QT_QPA_PLATFORM_PLUGIN_PATH')
+        self._copy_value_from_environ_if_set(clean_env, 'QT_WEBKIT_DISABLE_UIPROCESS_DUMPPIXELS')
+        return clean_env
+
+    # FIXME: We should find a way to share this implmentation with Gtk,
+    # or teach run-launcher how to call run-safari and move this down to Port.
+    def show_results_html_file(self, results_filename):
+        run_launcher_args = []
+        if self.get_option('webkit_test_runner'):
+            run_launcher_args.append('-2')
+        run_launcher_args.append("file://%s" % results_filename)
+        self._run_script("run-launcher", run_launcher_args)
+
+    def operating_system(self):
+        return self._operating_system
+
+    def check_sys_deps(self, needs_http):
+        result = super(QtPort, self).check_sys_deps(needs_http)
+        if not 'WEBKIT_TESTFONTS' in os.environ:
+            _log.error('\nThe WEBKIT_TESTFONTS environment variable is not defined or not set properly.')
+            _log.error('You must set it before running the tests.')
+            _log.error('Use git to grab the actual fonts from http://gitorious.org/qtwebkit/testfonts')
+            return False
+        return result
+
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/qt_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/qt_unittest.py
new file mode 100644
index 0000000..cf09bd8
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/qt_unittest.py
@@ -0,0 +1,132 @@
+# 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 os
+from copy import deepcopy
+
+from webkitpy.common.system.executive_mock import MockExecutive, MockExecutive2
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+from webkitpy.layout_tests.port import port_testcase
+from webkitpy.layout_tests.port.qt import QtPort
+from webkitpy.tool.mocktool import MockOptions
+
+
+class QtPortTest(port_testcase.PortTestCase):
+    port_name = 'qt-mac'
+    port_maker = QtPort
+    search_paths_cases = [
+        {'search_paths':['qt-4.8', 'qt-mac', 'qt'], 'os_name':'mac', 'use_webkit2':False, 'qt_version':'4.8'},
+        {'search_paths':['qt-4.8', 'qt-win', 'qt'], 'os_name':'win', 'use_webkit2':False, 'qt_version':'4.8'},
+        {'search_paths':['qt-4.8', 'qt-linux', 'qt'], 'os_name':'linux', 'use_webkit2':False, 'qt_version':'4.8'},
+
+        {'search_paths':['qt-4.8', 'qt-mac', 'qt'], 'os_name':'mac', 'use_webkit2':False},
+        {'search_paths':['qt-4.8', 'qt-win', 'qt'], 'os_name':'win', 'use_webkit2':False},
+        {'search_paths':['qt-4.8', 'qt-linux', 'qt'], 'os_name':'linux', 'use_webkit2':False},
+
+        {'search_paths':['qt-5.0-wk2', 'qt-5.0', 'qt-mac', 'qt'], 'os_name':'mac', 'use_webkit2':True, 'qt_version':'5.0'},
+        {'search_paths':['qt-5.0-wk2', 'qt-5.0', 'qt-win', 'qt'], 'os_name':'win', 'use_webkit2':True, 'qt_version':'5.0'},
+        {'search_paths':['qt-5.0-wk2', 'qt-5.0', 'qt-linux', 'qt'], 'os_name':'linux', 'use_webkit2':True, 'qt_version':'5.0'},
+
+        {'search_paths':['qt-5.0-wk1', 'qt-5.0', 'qt-mac', 'qt'], 'os_name':'mac', 'use_webkit2':False, 'qt_version':'5.0'},
+        {'search_paths':['qt-5.0-wk1', 'qt-5.0', 'qt-win', 'qt'], 'os_name':'win', 'use_webkit2':False, 'qt_version':'5.0'},
+        {'search_paths':['qt-5.0-wk1', 'qt-5.0', 'qt-linux', 'qt'], 'os_name':'linux', 'use_webkit2':False, 'qt_version':'5.0'},
+    ]
+
+    def _assert_search_path(self, search_paths, os_name, use_webkit2=False, qt_version='4.8'):
+        # FIXME: Port constructors should not "parse" the port name, but
+        # rather be passed components (directly or via setters).  Once
+        # we fix that, this method will need a re-write.
+        host = MockSystemHost(os_name=os_name)
+        host.executive = MockExecutive2(self._qt_version(qt_version))
+        port_name = 'qt-' + os_name
+        port = self.make_port(host=host, qt_version=qt_version, port_name=port_name,
+                              options=MockOptions(webkit_test_runner=use_webkit2, platform='qt'))
+        absolute_search_paths = map(port._webkit_baseline_path, search_paths)
+        self.assertEquals(port.baseline_search_path(), absolute_search_paths)
+
+    def _assert_expectations_files(self, search_paths, os_name, use_webkit2=False, qt_version='4.8'):
+        # FIXME: Port constructors should not "parse" the port name, but
+        # rather be passed components (directly or via setters).  Once
+        # we fix that, this method will need a re-write.
+        host = MockSystemHost(os_name=os_name)
+        host.executive = MockExecutive2(self._qt_version(qt_version))
+        port_name = 'qt-' + os_name
+        port = self.make_port(host=host, qt_version=qt_version, port_name=port_name,
+                              options=MockOptions(webkit_test_runner=use_webkit2, platform='qt'))
+        self.assertEquals(port.expectations_files(), search_paths)
+
+    def _qt_version(self, qt_version):
+        if qt_version in '4.8':
+            return 'QMake version 2.01a\nUsing Qt version 4.8.0 in /usr/local/Trolltech/Qt-4.8.2/lib'
+        if qt_version in '5.0':
+            return 'QMake version 2.01a\nUsing Qt version 5.0.0 in /usr/local/Trolltech/Qt-5.0.0/lib'
+
+    def test_baseline_search_path(self):
+        for case in self.search_paths_cases:
+            self._assert_search_path(**case)
+
+    def test_expectations_files(self):
+        for case in self.search_paths_cases:
+            expectations_case = deepcopy(case)
+            if expectations_case['use_webkit2']:
+                expectations_case['search_paths'].append("wk2")
+            expectations_case['search_paths'].reverse()
+            expectations_case['search_paths'] = map(lambda path: '/mock-checkout/LayoutTests/platform/%s/TestExpectations' % (path), expectations_case['search_paths'])
+            self._assert_expectations_files(**expectations_case)
+
+    def test_show_results_html_file(self):
+        port = self.make_port()
+        port._executive = MockExecutive(should_log=True)
+        expected_stderr = "MOCK run_command: ['Tools/Scripts/run-launcher', '--release', '--qt', 'file://test.html'], cwd=/mock-checkout\n"
+        OutputCapture().assert_outputs(self, port.show_results_html_file, ["test.html"], expected_stderr=expected_stderr)
+
+    def test_setup_environ_for_server(self):
+        port = self.make_port()
+        env = port.setup_environ_for_server(port.driver_name())
+        self.assertEquals(env['QTWEBKIT_PLUGIN_PATH'], '/mock-build/lib/plugins')
+
+    def test_operating_system(self):
+        self.assertEqual('linux', self.make_port(port_name='qt-linux', os_name='linux').operating_system())
+        self.assertEqual('mac', self.make_port(os_name='mac').operating_system())
+        self.assertEqual('win', self.make_port(port_name='qt-win', os_name='win').operating_system())
+
+    def test_check_sys_deps(self):
+        port = self.make_port()
+
+        # Success
+        os.environ['WEBKIT_TESTFONTS'] = '/tmp/foo'
+        port._executive = MockExecutive2(exit_code=0)
+        self.assertTrue(port.check_sys_deps(needs_http=False))
+
+        # Failure
+        del os.environ['WEBKIT_TESTFONTS']
+        port._executive = MockExecutive2(exit_code=1,
+            output='testing output failure')
+        self.assertFalse(port.check_sys_deps(needs_http=False))
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/server_process.py b/Tools/Scripts/webkitpy/layout_tests/port/server_process.py
new file mode 100644
index 0000000..8f0cda9
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/server_process.py
@@ -0,0 +1,380 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Package that implements the ServerProcess wrapper class"""
+
+import errno
+import logging
+import signal
+import sys
+import time
+
+# Note that although win32 python does provide an implementation of
+# the win32 select API, it only works on sockets, and not on the named pipes
+# used by subprocess, so we have to use the native APIs directly.
+if sys.platform == 'win32':
+    import msvcrt
+    import win32pipe
+    import win32file
+else:
+    import fcntl
+    import os
+    import select
+
+from webkitpy.common.system.executive import ScriptError
+
+
+_log = logging.getLogger(__name__)
+
+
+class ServerProcess(object):
+    """This class provides a wrapper around a subprocess that
+    implements a simple request/response usage model. The primary benefit
+    is that reading responses takes a deadline, so that we don't ever block
+    indefinitely. The class also handles transparently restarting processes
+    as necessary to keep issuing commands."""
+
+    def __init__(self, port_obj, name, cmd, env=None, universal_newlines=False, treat_no_data_as_crash=False):
+        self._port = port_obj
+        self._name = name  # Should be the command name (e.g. DumpRenderTree, ImageDiff)
+        self._cmd = cmd
+        self._env = env
+        # Set if the process outputs non-standard newlines like '\r\n' or '\r'.
+        # Don't set if there will be binary data or the data must be ASCII encoded.
+        self._universal_newlines = universal_newlines
+        self._treat_no_data_as_crash = treat_no_data_as_crash
+        self._host = self._port.host
+        self._pid = None
+        self._reset()
+
+        # See comment in imports for why we need the win32 APIs and can't just use select.
+        # FIXME: there should be a way to get win32 vs. cygwin from platforminfo.
+        self._use_win32_apis = sys.platform == 'win32'
+
+    def name(self):
+        return self._name
+
+    def pid(self):
+        return self._pid
+
+    def _reset(self):
+        if getattr(self, '_proc', None):
+            if self._proc.stdin:
+                self._proc.stdin.close()
+                self._proc.stdin = None
+            if self._proc.stdout:
+                self._proc.stdout.close()
+                self._proc.stdout = None
+            if self._proc.stderr:
+                self._proc.stderr.close()
+                self._proc.stderr = None
+
+        self._proc = None
+        self._output = str()  # bytesarray() once we require Python 2.6
+        self._error = str()  # bytesarray() once we require Python 2.6
+        self._crashed = False
+        self.timed_out = False
+
+    def process_name(self):
+        return self._name
+
+    def _start(self):
+        if self._proc:
+            raise ValueError("%s already running" % self._name)
+        self._reset()
+        # close_fds is a workaround for http://bugs.python.org/issue2320
+        close_fds = not self._host.platform.is_win()
+        self._proc = self._host.executive.popen(self._cmd, stdin=self._host.executive.PIPE,
+            stdout=self._host.executive.PIPE,
+            stderr=self._host.executive.PIPE,
+            close_fds=close_fds,
+            env=self._env,
+            universal_newlines=self._universal_newlines)
+        self._pid = self._proc.pid
+        fd = self._proc.stdout.fileno()
+        if not self._use_win32_apis:
+            fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+            fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+            fd = self._proc.stderr.fileno()
+            fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+            fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+
+    def _handle_possible_interrupt(self):
+        """This routine checks to see if the process crashed or exited
+        because of a keyboard interrupt and raises KeyboardInterrupt
+        accordingly."""
+        # FIXME: Linux and Mac set the returncode to -signal.SIGINT if a
+        # subprocess is killed with a ctrl^C.  Previous comments in this
+        # routine said that supposedly Windows returns 0xc000001d, but that's not what
+        # -1073741510 evaluates to. Figure out what the right value is
+        # for win32 and cygwin here ...
+        if self._proc.returncode in (-1073741510, -signal.SIGINT):
+            raise KeyboardInterrupt
+
+    def poll(self):
+        """Check to see if the underlying process is running; returns None
+        if it still is (wrapper around subprocess.poll)."""
+        if self._proc:
+            return self._proc.poll()
+        return None
+
+    def write(self, bytes):
+        """Write a request to the subprocess. The subprocess is (re-)start()'ed
+        if is not already running."""
+        if not self._proc:
+            self._start()
+        try:
+            self._proc.stdin.write(bytes)
+        except IOError, e:
+            self.stop(0.0)
+            # stop() calls _reset(), so we have to set crashed to True after calling stop().
+            self._crashed = True
+
+    def _pop_stdout_line_if_ready(self):
+        index_after_newline = self._output.find('\n') + 1
+        if index_after_newline > 0:
+            return self._pop_output_bytes(index_after_newline)
+        return None
+
+    def _pop_stderr_line_if_ready(self):
+        index_after_newline = self._error.find('\n') + 1
+        if index_after_newline > 0:
+            return self._pop_error_bytes(index_after_newline)
+        return None
+
+    def pop_all_buffered_stderr(self):
+        return self._pop_error_bytes(len(self._error))
+
+    def read_stdout_line(self, deadline):
+        return self._read(deadline, self._pop_stdout_line_if_ready)
+
+    def read_stderr_line(self, deadline):
+        return self._read(deadline, self._pop_stderr_line_if_ready)
+
+    def read_either_stdout_or_stderr_line(self, deadline):
+        def retrieve_bytes_from_buffers():
+            stdout_line = self._pop_stdout_line_if_ready()
+            if stdout_line:
+                return stdout_line, None
+            stderr_line = self._pop_stderr_line_if_ready()
+            if stderr_line:
+                return None, stderr_line
+            return None  # Instructs the caller to keep waiting.
+
+        return_value = self._read(deadline, retrieve_bytes_from_buffers)
+        # FIXME: This is a bit of a hack around the fact that _read normally only returns one value, but this caller wants it to return two.
+        if return_value is None:
+            return None, None
+        return return_value
+
+    def read_stdout(self, deadline, size):
+        if size <= 0:
+            raise ValueError('ServerProcess.read() called with a non-positive size: %d ' % size)
+
+        def retrieve_bytes_from_stdout_buffer():
+            if len(self._output) >= size:
+                return self._pop_output_bytes(size)
+            return None
+
+        return self._read(deadline, retrieve_bytes_from_stdout_buffer)
+
+    def _log(self, message):
+        # This is a bit of a hack, but we first log a blank line to avoid
+        # messing up the master process's output.
+        _log.info('')
+        _log.info(message)
+
+    def _handle_timeout(self):
+        self.timed_out = True
+        self._port.sample_process(self._name, self._proc.pid)
+
+    def _split_string_after_index(self, string, index):
+        return string[:index], string[index:]
+
+    def _pop_output_bytes(self, bytes_count):
+        output, self._output = self._split_string_after_index(self._output, bytes_count)
+        return output
+
+    def _pop_error_bytes(self, bytes_count):
+        output, self._error = self._split_string_after_index(self._error, bytes_count)
+        return output
+
+    def _wait_for_data_and_update_buffers_using_select(self, deadline, stopping=False):
+        if self._proc.stdout.closed or self._proc.stderr.closed:
+            # If the process crashed and is using FIFOs, like Chromium Android, the
+            # stdout and stderr pipes will be closed.
+            return
+
+        out_fd = self._proc.stdout.fileno()
+        err_fd = self._proc.stderr.fileno()
+        select_fds = (out_fd, err_fd)
+        try:
+            read_fds, _, _ = select.select(select_fds, [], select_fds, max(deadline - time.time(), 0))
+        except select.error, e:
+            # We can ignore EINVAL since it's likely the process just crashed and we'll
+            # figure that out the next time through the loop in _read().
+            if e.args[0] == errno.EINVAL:
+                return
+            raise
+
+        try:
+            # Note that we may get no data during read() even though
+            # select says we got something; see the select() man page
+            # on linux. I don't know if this happens on Mac OS and
+            # other Unixen as well, but we don't bother special-casing
+            # Linux because it's relatively harmless either way.
+            if out_fd in read_fds:
+                data = self._proc.stdout.read()
+                if not data and not stopping and (self._treat_no_data_as_crash or self._proc.poll()):
+                    self._crashed = True
+                self._output += data
+
+            if err_fd in read_fds:
+                data = self._proc.stderr.read()
+                if not data and not stopping and (self._treat_no_data_as_crash or self._proc.poll()):
+                    self._crashed = True
+                self._error += data
+        except IOError, e:
+            # We can ignore the IOErrors because we will detect if the subporcess crashed
+            # the next time through the loop in _read()
+            pass
+
+    def _wait_for_data_and_update_buffers_using_win32_apis(self, deadline):
+        # See http://code.activestate.com/recipes/440554-module-to-allow-asynchronous-subprocess-use-on-win/
+        # and http://docs.activestate.com/activepython/2.6/pywin32/modules.html
+        # for documentation on all of these win32-specific modules.
+        now = time.time()
+        out_fh = msvcrt.get_osfhandle(self._proc.stdout.fileno())
+        err_fh = msvcrt.get_osfhandle(self._proc.stderr.fileno())
+        while (self._proc.poll() is None) and (now < deadline):
+            output = self._non_blocking_read_win32(out_fh)
+            error = self._non_blocking_read_win32(err_fh)
+            if output or error:
+                if output:
+                    self._output += output
+                if error:
+                    self._error += error
+                return
+            time.sleep(0.01)
+            now = time.time()
+        return
+
+    def _non_blocking_read_win32(self, handle):
+        try:
+            _, avail, _ = win32pipe.PeekNamedPipe(handle, 0)
+            if avail > 0:
+                _, buf = win32file.ReadFile(handle, avail, None)
+                return buf
+        except Exception, e:
+            if e[0] not in (109, errno.ESHUTDOWN):  # 109 == win32 ERROR_BROKEN_PIPE
+                raise
+        return None
+
+    def has_crashed(self):
+        if not self._crashed and self.poll():
+            self._crashed = True
+            self._handle_possible_interrupt()
+        return self._crashed
+
+    # This read function is a bit oddly-designed, as it polls both stdout and stderr, yet
+    # only reads/returns from one of them (buffering both in local self._output/self._error).
+    # It might be cleaner to pass in the file descriptor to poll instead.
+    def _read(self, deadline, fetch_bytes_from_buffers_callback):
+        while True:
+            if self.has_crashed():
+                return None
+
+            if time.time() > deadline:
+                self._handle_timeout()
+                return None
+
+            bytes = fetch_bytes_from_buffers_callback()
+            if bytes is not None:
+                return bytes
+
+            if self._use_win32_apis:
+                self._wait_for_data_and_update_buffers_using_win32_apis(deadline)
+            else:
+                self._wait_for_data_and_update_buffers_using_select(deadline)
+
+    def start(self):
+        if not self._proc:
+            self._start()
+
+    def stop(self, timeout_secs=3.0):
+        if not self._proc:
+            return (None, None)
+
+        # Only bother to check for leaks or stderr if the process is still running.
+        if self.poll() is None:
+            self._port.check_for_leaks(self.name(), self.pid())
+
+        now = time.time()
+        if self._proc.stdin:
+            self._proc.stdin.close()
+            self._proc.stdin = None
+        killed = False
+        if timeout_secs:
+            deadline = now + timeout_secs
+            while self._proc.poll() is None and time.time() < deadline:
+                time.sleep(0.01)
+            if self._proc.poll() is None:
+                _log.warning('stopping %s(pid %d) timed out, killing it' % (self._name, self._proc.pid))
+
+        if self._proc.poll() is None:
+            self._kill()
+            killed = True
+            _log.debug('killed pid %d' % self._proc.pid)
+
+        # read any remaining data on the pipes and return it.
+        if not killed:
+            if self._use_win32_apis:
+                self._wait_for_data_and_update_buffers_using_win32_apis(now)
+            else:
+                self._wait_for_data_and_update_buffers_using_select(now, stopping=True)
+        out, err = self._output, self._error
+        self._reset()
+        return (out, err)
+
+    def kill(self):
+        self.stop(0.0)
+
+    def _kill(self):
+        self._host.executive.kill_process(self._proc.pid)
+        if self._proc.poll() is not None:
+            self._proc.wait()
+
+    def replace_outputs(self, stdout, stderr):
+        assert self._proc
+        if stdout:
+            self._proc.stdout.close()
+            self._proc.stdout = stdout
+        if stderr:
+            self._proc.stderr.close()
+            self._proc.stderr = stderr
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/server_process_mock.py b/Tools/Scripts/webkitpy/layout_tests/port/server_process_mock.py
new file mode 100644
index 0000000..d234ebd
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/server_process_mock.py
@@ -0,0 +1,77 @@
+# Copyright (C) 2012 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 MockServerProcess(object):
+    def __init__(self, port_obj=None, name=None, cmd=None, env=None, universal_newlines=False, lines=None, crashed=False):
+        self.timed_out = False
+        self.lines = lines or []
+        self.crashed = crashed
+        self.writes = []
+        self.cmd = cmd
+        self.env = env
+        self.started = False
+        self.stopped = False
+
+    def write(self, bytes):
+        self.writes.append(bytes)
+
+    def has_crashed(self):
+        return self.crashed
+
+    def read_stdout_line(self, deadline):
+        return self.lines.pop(0) + "\n"
+
+    def read_stdout(self, deadline, size):
+        first_line = self.lines[0]
+        if size > len(first_line):
+            self.lines.pop(0)
+            remaining_size = size - len(first_line) - 1
+            if not remaining_size:
+                return first_line + "\n"
+            return first_line + "\n" + self.read_stdout(deadline, remaining_size)
+        result = self.lines[0][:size]
+        self.lines[0] = self.lines[0][size:]
+        return result
+
+    def pop_all_buffered_stderr(self):
+        return ''
+
+    def read_either_stdout_or_stderr_line(self, deadline):
+        # FIXME: We should have tests which intermix stderr and stdout lines.
+        return self.read_stdout_line(deadline), None
+
+    def start(self):
+        self.started = True
+
+    def stop(self, kill_directly=False):
+        self.stopped = True
+        return
+
+    def kill(self):
+        return
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/server_process_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/server_process_unittest.py
new file mode 100644
index 0000000..7a5ac45
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/server_process_unittest.py
@@ -0,0 +1,152 @@
+# Copyright (C) 2011 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 sys
+import time
+import unittest
+
+from webkitpy.layout_tests.port.factory import PortFactory
+from webkitpy.layout_tests.port import server_process
+from webkitpy.common.system.systemhost import SystemHost
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+from webkitpy.common.system.outputcapture import OutputCapture
+
+
+
+class TrivialMockPort(object):
+    def __init__(self):
+        self.host = MockSystemHost()
+        self.host.executive.kill_process = lambda x: None
+        self.host.executive.kill_process = lambda x: None
+
+    def results_directory(self):
+        return "/mock-results"
+
+    def check_for_leaks(self, process_name, process_pid):
+        pass
+
+    def process_kill_time(self):
+        return 1
+
+
+class MockFile(object):
+    def __init__(self, server_process):
+        self._server_process = server_process
+        self.closed = False
+
+    def fileno(self):
+        return 1
+
+    def write(self, line):
+        self._server_process.broken_pipes.append(self)
+        raise IOError
+
+    def close(self):
+        self.closed = True
+
+
+class MockProc(object):
+    def __init__(self, server_process):
+        self.stdin = MockFile(server_process)
+        self.stdout = MockFile(server_process)
+        self.stderr = MockFile(server_process)
+        self.pid = 1
+
+    def poll(self):
+        return 1
+
+    def wait(self):
+        return 0
+
+
+class FakeServerProcess(server_process.ServerProcess):
+    def _start(self):
+        self._proc = MockProc(self)
+        self.stdin = self._proc.stdin
+        self.stdout = self._proc.stdout
+        self.stderr = self._proc.stderr
+        self._pid = self._proc.pid
+        self.broken_pipes = []
+
+
+class TestServerProcess(unittest.TestCase):
+    def test_basic(self):
+        cmd = [sys.executable, '-c', 'import sys; import time; time.sleep(0.02); print "stdout"; sys.stdout.flush(); print >>sys.stderr, "stderr"']
+        host = SystemHost()
+        factory = PortFactory(host)
+        port = factory.get()
+        now = time.time()
+        proc = server_process.ServerProcess(port, 'python', cmd)
+        proc.write('')
+
+        self.assertEquals(proc.poll(), None)
+        self.assertFalse(proc.has_crashed())
+
+        # check that doing a read after an expired deadline returns
+        # nothing immediately.
+        line = proc.read_stdout_line(now - 1)
+        self.assertEquals(line, None)
+
+        # FIXME: This part appears to be flaky. line should always be non-None.
+        # FIXME: https://bugs.webkit.org/show_bug.cgi?id=88280
+        line = proc.read_stdout_line(now + 1.0)
+        if line:
+            self.assertEquals(line.strip(), "stdout")
+
+        line = proc.read_stderr_line(now + 1.0)
+        if line:
+            self.assertEquals(line.strip(), "stderr")
+
+        proc.stop(0)
+
+    def test_cleanup(self):
+        port_obj = TrivialMockPort()
+        server_process = FakeServerProcess(port_obj=port_obj, name="test", cmd=["test"])
+        server_process._start()
+        server_process.stop()
+        self.assertTrue(server_process.stdin.closed)
+        self.assertTrue(server_process.stdout.closed)
+        self.assertTrue(server_process.stderr.closed)
+
+    def test_broken_pipe(self):
+        port_obj = TrivialMockPort()
+
+        port_obj.host.platform.os_name = 'win'
+        server_process = FakeServerProcess(port_obj=port_obj, name="test", cmd=["test"])
+        server_process.write("should break")
+        self.assertTrue(server_process.has_crashed())
+        self.assertNotEquals(server_process.pid(), None)
+        self.assertEquals(server_process._proc, None)
+        self.assertEquals(server_process.broken_pipes, [server_process.stdin])
+
+        port_obj.host.platform.os_name = 'mac'
+        server_process = FakeServerProcess(port_obj=port_obj, name="test", cmd=["test"])
+        server_process.write("should break")
+        self.assertTrue(server_process.has_crashed())
+        self.assertEquals(server_process._proc, None)
+        self.assertEquals(server_process.broken_pipes, [server_process.stdin])
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/test.py b/Tools/Scripts/webkitpy/layout_tests/port/test.py
new file mode 100644
index 0000000..f7dd291
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/test.py
@@ -0,0 +1,598 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import base64
+import sys
+import time
+
+from webkitpy.layout_tests.port import Port, Driver, DriverOutput
+from webkitpy.layout_tests.port.base import VirtualTestSuite
+from webkitpy.layout_tests.models.test_configuration import TestConfiguration
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.crashlogs import CrashLogs
+
+
+# This sets basic expectations for a test. Each individual expectation
+# can be overridden by a keyword argument in TestList.add().
+class TestInstance(object):
+    def __init__(self, name):
+        self.name = name
+        self.base = name[(name.rfind("/") + 1):name.rfind(".")]
+        self.crash = False
+        self.web_process_crash = False
+        self.exception = False
+        self.hang = False
+        self.keyboard = False
+        self.error = ''
+        self.timeout = False
+        self.is_reftest = False
+
+        # The values of each field are treated as raw byte strings. They
+        # will be converted to unicode strings where appropriate using
+        # FileSystem.read_text_file().
+        self.actual_text = self.base + '-txt'
+        self.actual_checksum = self.base + '-checksum'
+
+        # We add the '\x8a' for the image file to prevent the value from
+        # being treated as UTF-8 (the character is invalid)
+        self.actual_image = self.base + '\x8a' + '-png' + 'tEXtchecksum\x00' + self.actual_checksum
+
+        self.expected_text = self.actual_text
+        self.expected_image = self.actual_image
+
+        self.actual_audio = None
+        self.expected_audio = None
+
+
+# This is an in-memory list of tests, what we want them to produce, and
+# what we want to claim are the expected results.
+class TestList(object):
+    def __init__(self):
+        self.tests = {}
+
+    def add(self, name, **kwargs):
+        test = TestInstance(name)
+        for key, value in kwargs.items():
+            test.__dict__[key] = value
+        self.tests[name] = test
+
+    def add_reftest(self, name, reference_name, same_image):
+        self.add(name, actual_checksum='xxx', actual_image='XXX', is_reftest=True)
+        if same_image:
+            self.add(reference_name, actual_checksum='xxx', actual_image='XXX', is_reftest=True)
+        else:
+            self.add(reference_name, actual_checksum='yyy', actual_image='YYY', is_reftest=True)
+
+    def keys(self):
+        return self.tests.keys()
+
+    def __contains__(self, item):
+        return item in self.tests
+
+    def __getitem__(self, item):
+        return self.tests[item]
+
+
+def unit_test_list():
+    tests = TestList()
+    tests.add('failures/expected/crash.html', crash=True)
+    tests.add('failures/expected/exception.html', exception=True)
+    tests.add('failures/expected/timeout.html', timeout=True)
+    tests.add('failures/expected/hang.html', hang=True)
+    tests.add('failures/expected/missing_text.html', expected_text=None)
+    tests.add('failures/expected/image.html',
+              actual_image='image_fail-pngtEXtchecksum\x00checksum_fail',
+              expected_image='image-pngtEXtchecksum\x00checksum-png')
+    tests.add('failures/expected/image_checksum.html',
+              actual_checksum='image_checksum_fail-checksum',
+              actual_image='image_checksum_fail-png')
+    tests.add('failures/expected/audio.html',
+              actual_audio=base64.b64encode('audio_fail-wav'), expected_audio='audio-wav',
+              actual_text=None, expected_text=None,
+              actual_image=None, expected_image=None,
+              actual_checksum=None)
+    tests.add('failures/expected/keyboard.html', keyboard=True)
+    tests.add('failures/expected/missing_check.html',
+              expected_image='missing_check-png')
+    tests.add('failures/expected/missing_image.html', expected_image=None)
+    tests.add('failures/expected/missing_audio.html', expected_audio=None,
+              actual_text=None, expected_text=None,
+              actual_image=None, expected_image=None,
+              actual_checksum=None)
+    tests.add('failures/expected/missing_text.html', expected_text=None)
+    tests.add('failures/expected/newlines_leading.html',
+              expected_text="\nfoo\n", actual_text="foo\n")
+    tests.add('failures/expected/newlines_trailing.html',
+              expected_text="foo\n\n", actual_text="foo\n")
+    tests.add('failures/expected/newlines_with_excess_CR.html',
+              expected_text="foo\r\r\r\n", actual_text="foo\n")
+    tests.add('failures/expected/text.html', actual_text='text_fail-png')
+    tests.add('failures/expected/skip_text.html', actual_text='text diff')
+    tests.add('failures/flaky/text.html')
+    tests.add('failures/unexpected/missing_text.html', expected_text=None)
+    tests.add('failures/unexpected/missing_check.html', expected_image='missing-check-png')
+    tests.add('failures/unexpected/missing_image.html', expected_image=None)
+    tests.add('failures/unexpected/missing_render_tree_dump.html', actual_text="""layer at (0,0) size 800x600
+  RenderView at (0,0) size 800x600
+layer at (0,0) size 800x34
+  RenderBlock {HTML} at (0,0) size 800x34
+    RenderBody {BODY} at (8,8) size 784x18
+      RenderText {#text} at (0,0) size 133x18
+        text run at (0,0) width 133: "This is an image test!"
+""", expected_text=None)
+    tests.add('failures/unexpected/crash.html', crash=True)
+    tests.add('failures/unexpected/crash-with-stderr.html', crash=True,
+              error="mock-std-error-output")
+    tests.add('failures/unexpected/web-process-crash-with-stderr.html', web_process_crash=True,
+              error="mock-std-error-output")
+    tests.add('failures/unexpected/pass.html')
+    tests.add('failures/unexpected/text-checksum.html',
+              actual_text='text-checksum_fail-txt',
+              actual_checksum='text-checksum_fail-checksum')
+    tests.add('failures/unexpected/text-image-checksum.html',
+              actual_text='text-image-checksum_fail-txt',
+              actual_image='text-image-checksum_fail-pngtEXtchecksum\x00checksum_fail',
+              actual_checksum='text-image-checksum_fail-checksum')
+    tests.add('failures/unexpected/checksum-with-matching-image.html',
+              actual_checksum='text-image-checksum_fail-checksum')
+    tests.add('failures/unexpected/skip_pass.html')
+    tests.add('failures/unexpected/text.html', actual_text='text_fail-txt')
+    tests.add('failures/unexpected/timeout.html', timeout=True)
+    tests.add('http/tests/passes/text.html')
+    tests.add('http/tests/passes/image.html')
+    tests.add('http/tests/ssl/text.html')
+    tests.add('passes/args.html')
+    tests.add('passes/error.html', error='stuff going to stderr')
+    tests.add('passes/image.html')
+    tests.add('passes/audio.html',
+              actual_audio=base64.b64encode('audio-wav'), expected_audio='audio-wav',
+              actual_text=None, expected_text=None,
+              actual_image=None, expected_image=None,
+              actual_checksum=None)
+    tests.add('passes/platform_image.html')
+    tests.add('passes/checksum_in_image.html',
+              expected_image='tEXtchecksum\x00checksum_in_image-checksum')
+    tests.add('passes/skipped/skip.html')
+
+    # Note that here the checksums don't match but the images do, so this test passes "unexpectedly".
+    # See https://bugs.webkit.org/show_bug.cgi?id=69444 .
+    tests.add('failures/unexpected/checksum.html', actual_checksum='checksum_fail-checksum')
+
+    # Text output files contain "\r\n" on Windows.  This may be
+    # helpfully filtered to "\r\r\n" by our Python/Cygwin tooling.
+    tests.add('passes/text.html',
+              expected_text='\nfoo\n\n', actual_text='\nfoo\r\n\r\r\n')
+
+    # For reftests.
+    tests.add_reftest('passes/reftest.html', 'passes/reftest-expected.html', same_image=True)
+    tests.add_reftest('passes/mismatch.html', 'passes/mismatch-expected-mismatch.html', same_image=False)
+    tests.add_reftest('passes/svgreftest.svg', 'passes/svgreftest-expected.svg', same_image=True)
+    tests.add_reftest('passes/xhtreftest.xht', 'passes/xhtreftest-expected.html', same_image=True)
+    tests.add_reftest('passes/phpreftest.php', 'passes/phpreftest-expected-mismatch.svg', same_image=False)
+    tests.add_reftest('failures/expected/reftest.html', 'failures/expected/reftest-expected.html', same_image=False)
+    tests.add_reftest('failures/expected/mismatch.html', 'failures/expected/mismatch-expected-mismatch.html', same_image=True)
+    tests.add_reftest('failures/unexpected/reftest.html', 'failures/unexpected/reftest-expected.html', same_image=False)
+    tests.add_reftest('failures/unexpected/mismatch.html', 'failures/unexpected/mismatch-expected-mismatch.html', same_image=True)
+    tests.add('failures/unexpected/reftest-nopixel.html', actual_checksum=None, actual_image=None, is_reftest=True)
+    tests.add('failures/unexpected/reftest-nopixel-expected.html', actual_checksum=None, actual_image=None, is_reftest=True)
+    # FIXME: Add a reftest which crashes.
+    tests.add('reftests/foo/test.html')
+    tests.add('reftests/foo/test-ref.html')
+
+    tests.add('reftests/foo/multiple-match-success.html', actual_checksum='abc', actual_image='abc')
+    tests.add('reftests/foo/multiple-match-failure.html', actual_checksum='abc', actual_image='abc')
+    tests.add('reftests/foo/multiple-mismatch-success.html', actual_checksum='abc', actual_image='abc')
+    tests.add('reftests/foo/multiple-mismatch-failure.html', actual_checksum='abc', actual_image='abc')
+    tests.add('reftests/foo/multiple-both-success.html', actual_checksum='abc', actual_image='abc')
+    tests.add('reftests/foo/multiple-both-failure.html', actual_checksum='abc', actual_image='abc')
+
+    tests.add('reftests/foo/matching-ref.html', actual_checksum='abc', actual_image='abc')
+    tests.add('reftests/foo/mismatching-ref.html', actual_checksum='def', actual_image='def')
+    tests.add('reftests/foo/second-mismatching-ref.html', actual_checksum='ghi', actual_image='ghi')
+
+    # The following files shouldn't be treated as reftests
+    tests.add_reftest('reftests/foo/unlistedtest.html', 'reftests/foo/unlistedtest-expected.html', same_image=True)
+    tests.add('reftests/foo/reference/bar/common.html')
+    tests.add('reftests/foo/reftest/bar/shared.html')
+
+    tests.add('websocket/tests/passes/text.html')
+
+    # For testing test are properly included from platform directories.
+    tests.add('platform/test-mac-leopard/http/test.html')
+    tests.add('platform/test-win-win7/http/test.html')
+
+    # For --no-http tests, test that platform specific HTTP tests are properly skipped.
+    tests.add('platform/test-snow-leopard/http/test.html')
+    tests.add('platform/test-snow-leopard/websocket/test.html')
+
+    # For testing if perf tests are running in a locked shard.
+    tests.add('perf/foo/test.html')
+    tests.add('perf/foo/test-ref.html')
+
+    # For testing --pixel-test-directories.
+    tests.add('failures/unexpected/pixeldir/image_in_pixeldir.html',
+        actual_image='image_in_pixeldir-pngtEXtchecksum\x00checksum_fail',
+        expected_image='image_in_pixeldir-pngtEXtchecksum\x00checksum-png')
+    tests.add('failures/unexpected/image_not_in_pixeldir.html',
+        actual_image='image_not_in_pixeldir-pngtEXtchecksum\x00checksum_fail',
+        expected_image='image_not_in_pixeldir-pngtEXtchecksum\x00checksum-png')
+
+    # For testing that virtual test suites don't expand names containing themselves
+    # See webkit.org/b/97925 and base_unittest.PortTest.test_tests().
+    tests.add('passes/test-virtual-passes.html')
+    tests.add('passes/passes/test-virtual-passes.html')
+
+    return tests
+
+
+# Here we use a non-standard location for the layout tests, to ensure that
+# this works. The path contains a '.' in the name because we've seen bugs
+# related to this before.
+
+LAYOUT_TEST_DIR = '/test.checkout/LayoutTests'
+PERF_TEST_DIR = '/test.checkout/PerformanceTests'
+
+
+# Here we synthesize an in-memory filesystem from the test list
+# in order to fully control the test output and to demonstrate that
+# we don't need a real filesystem to run the tests.
+def add_unit_tests_to_mock_filesystem(filesystem):
+    # Add the test_expectations file.
+    filesystem.maybe_make_directory(LAYOUT_TEST_DIR + '/platform/test')
+    if not filesystem.exists(LAYOUT_TEST_DIR + '/platform/test/TestExpectations'):
+        filesystem.write_text_file(LAYOUT_TEST_DIR + '/platform/test/TestExpectations', """
+Bug(test) failures/expected/crash.html [ Crash ]
+Bug(test) failures/expected/image.html [ ImageOnlyFailure ]
+Bug(test) failures/expected/audio.html [ Failure ]
+Bug(test) failures/expected/image_checksum.html [ ImageOnlyFailure ]
+Bug(test) failures/expected/mismatch.html [ ImageOnlyFailure ]
+Bug(test) failures/expected/missing_check.html [ Missing Pass ]
+Bug(test) failures/expected/missing_image.html [ Missing Pass ]
+Bug(test) failures/expected/missing_audio.html [ Missing Pass ]
+Bug(test) failures/expected/missing_text.html [ Missing Pass ]
+Bug(test) failures/expected/newlines_leading.html [ Failure ]
+Bug(test) failures/expected/newlines_trailing.html [ Failure ]
+Bug(test) failures/expected/newlines_with_excess_CR.html [ Failure ]
+Bug(test) failures/expected/reftest.html [ ImageOnlyFailure ]
+Bug(test) failures/expected/text.html [ Failure ]
+Bug(test) failures/expected/timeout.html [ Timeout ]
+Bug(test) failures/expected/hang.html [ WontFix ]
+Bug(test) failures/expected/keyboard.html [ WontFix ]
+Bug(test) failures/expected/exception.html [ WontFix ]
+Bug(test) failures/unexpected/pass.html [ Failure ]
+Bug(test) passes/skipped/skip.html [ Skip ]
+""")
+
+    filesystem.maybe_make_directory(LAYOUT_TEST_DIR + '/reftests/foo')
+    filesystem.write_text_file(LAYOUT_TEST_DIR + '/reftests/foo/reftest.list', """
+== test.html test-ref.html
+
+== multiple-match-success.html mismatching-ref.html
+== multiple-match-success.html matching-ref.html
+== multiple-match-failure.html mismatching-ref.html
+== multiple-match-failure.html second-mismatching-ref.html
+!= multiple-mismatch-success.html mismatching-ref.html
+!= multiple-mismatch-success.html second-mismatching-ref.html
+!= multiple-mismatch-failure.html mismatching-ref.html
+!= multiple-mismatch-failure.html matching-ref.html
+== multiple-both-success.html matching-ref.html
+== multiple-both-success.html mismatching-ref.html
+!= multiple-both-success.html second-mismatching-ref.html
+== multiple-both-failure.html matching-ref.html
+!= multiple-both-failure.html second-mismatching-ref.html
+!= multiple-both-failure.html matching-ref.html
+""")
+
+    # FIXME: This test was only being ignored because of missing a leading '/'.
+    # Fixing the typo causes several tests to assert, so disabling the test entirely.
+    # Add in a file should be ignored by port.find_test_files().
+    #files[LAYOUT_TEST_DIR + '/userscripts/resources/iframe.html'] = 'iframe'
+
+    def add_file(test, suffix, contents):
+        dirname = filesystem.join(LAYOUT_TEST_DIR, test.name[0:test.name.rfind('/')])
+        base = test.base
+        filesystem.maybe_make_directory(dirname)
+        filesystem.write_binary_file(filesystem.join(dirname, base + suffix), contents)
+
+    # Add each test and the expected output, if any.
+    test_list = unit_test_list()
+    for test in test_list.tests.values():
+        add_file(test, test.name[test.name.rfind('.'):], '')
+        if test.is_reftest:
+            continue
+        if test.actual_audio:
+            add_file(test, '-expected.wav', test.expected_audio)
+            continue
+        add_file(test, '-expected.txt', test.expected_text)
+        add_file(test, '-expected.png', test.expected_image)
+
+    filesystem.write_text_file(filesystem.join(LAYOUT_TEST_DIR, 'virtual', 'passes', 'args-expected.txt'), 'args-txt --virtual-arg')
+    # Clear the list of written files so that we can watch what happens during testing.
+    filesystem.clear_written_files()
+
+
+class TestPort(Port):
+    port_name = 'test'
+
+    """Test implementation of the Port interface."""
+    ALL_BASELINE_VARIANTS = (
+        'test-linux-x86_64',
+        'test-mac-snowleopard', 'test-mac-leopard',
+        'test-win-vista', 'test-win-win7', 'test-win-xp',
+    )
+
+    @classmethod
+    def determine_full_port_name(cls, host, options, port_name):
+        if port_name == 'test':
+            return 'test-mac-leopard'
+        return port_name
+
+    def __init__(self, host, port_name=None, **kwargs):
+        # FIXME: Consider updating all of the callers to pass in a port_name so it can be a
+        # required parameter like all of the other Port objects.
+        port_name = port_name or 'test-mac-leopard'
+        Port.__init__(self, host, port_name, **kwargs)
+        self._tests = unit_test_list()
+        self._flakes = set()
+        self._expectations_path = LAYOUT_TEST_DIR + '/platform/test/TestExpectations'
+        self._results_directory = None
+
+        self._operating_system = 'mac'
+        if port_name.startswith('test-win'):
+            self._operating_system = 'win'
+        elif port_name.startswith('test-linux'):
+            self._operating_system = 'linux'
+
+        version_map = {
+            'test-win-xp': 'xp',
+            'test-win-win7': 'win7',
+            'test-win-vista': 'vista',
+            'test-mac-leopard': 'leopard',
+            'test-mac-snowleopard': 'snowleopard',
+            'test-linux-x86_64': 'lucid',
+        }
+        self._version = version_map[port_name]
+
+    def default_pixel_tests(self):
+        return True
+
+    def _path_to_driver(self):
+        # This routine shouldn't normally be called, but it is called by
+        # the mock_drt Driver. We return something, but make sure it's useless.
+        return 'MOCK _path_to_driver'
+
+    def baseline_search_path(self):
+        search_paths = {
+            'test-mac-snowleopard': ['test-mac-snowleopard'],
+            'test-mac-leopard': ['test-mac-leopard', 'test-mac-snowleopard'],
+            'test-win-win7': ['test-win-win7'],
+            'test-win-vista': ['test-win-vista', 'test-win-win7'],
+            'test-win-xp': ['test-win-xp', 'test-win-vista', 'test-win-win7'],
+            'test-linux-x86_64': ['test-linux', 'test-win-win7'],
+        }
+        return [self._webkit_baseline_path(d) for d in search_paths[self.name()]]
+
+    def default_child_processes(self):
+        return 1
+
+    def worker_startup_delay_secs(self):
+        return 0
+
+    def check_build(self, needs_http):
+        return True
+
+    def check_sys_deps(self, needs_http):
+        return True
+
+    def default_configuration(self):
+        return 'Release'
+
+    def diff_image(self, expected_contents, actual_contents, tolerance=None):
+        diffed = actual_contents != expected_contents
+        if not actual_contents and not expected_contents:
+            return (None, 0, None)
+        if not actual_contents or not expected_contents:
+            return (True, 0, None)
+        if 'ref' in expected_contents:
+            assert tolerance == 0
+        if diffed:
+            return ("< %s\n---\n> %s\n" % (expected_contents, actual_contents), 1, None)
+        return (None, 0, None)
+
+    def layout_tests_dir(self):
+        return LAYOUT_TEST_DIR
+
+    def perf_tests_dir(self):
+        return PERF_TEST_DIR
+
+    def webkit_base(self):
+        return '/test.checkout'
+
+    def skipped_layout_tests(self, test_list):
+        # This allows us to test the handling Skipped files, both with a test
+        # that actually passes, and a test that does fail.
+        return set(['failures/expected/skip_text.html',
+                    'failures/unexpected/skip_pass.html',
+                    'virtual/skipped'])
+
+    def name(self):
+        return self._name
+
+    def operating_system(self):
+        return self._operating_system
+
+    def _path_to_wdiff(self):
+        return None
+
+    def default_results_directory(self):
+        return '/tmp/layout-test-results'
+
+    def setup_test_run(self):
+        pass
+
+    def _driver_class(self):
+        return TestDriver
+
+    def start_http_server(self, additional_dirs=None, number_of_servers=None):
+        pass
+
+    def start_websocket_server(self):
+        pass
+
+    def acquire_http_lock(self):
+        pass
+
+    def stop_http_server(self):
+        pass
+
+    def stop_websocket_server(self):
+        pass
+
+    def release_http_lock(self):
+        pass
+
+    def _path_to_lighttpd(self):
+        return "/usr/sbin/lighttpd"
+
+    def _path_to_lighttpd_modules(self):
+        return "/usr/lib/lighttpd"
+
+    def _path_to_lighttpd_php(self):
+        return "/usr/bin/php-cgi"
+
+    def _path_to_apache(self):
+        return "/usr/sbin/httpd"
+
+    def _path_to_apache_config_file(self):
+        return self._filesystem.join(self.layout_tests_dir(), 'http', 'conf', 'httpd.conf')
+
+    def path_to_test_expectations_file(self):
+        return self._expectations_path
+
+    def all_test_configurations(self):
+        """Returns a sequence of the TestConfigurations the port supports."""
+        # By default, we assume we want to test every graphics type in
+        # every configuration on every system.
+        test_configurations = []
+        for version, architecture in self._all_systems():
+            for build_type in self._all_build_types():
+                test_configurations.append(TestConfiguration(
+                    version=version,
+                    architecture=architecture,
+                    build_type=build_type))
+        return test_configurations
+
+    def _all_systems(self):
+        return (('leopard', 'x86'),
+                ('snowleopard', 'x86'),
+                ('xp', 'x86'),
+                ('vista', 'x86'),
+                ('win7', 'x86'),
+                ('lucid', 'x86'),
+                ('lucid', 'x86_64'))
+
+    def _all_build_types(self):
+        return ('debug', 'release')
+
+    def configuration_specifier_macros(self):
+        """To avoid surprises when introducing new macros, these are intentionally fixed in time."""
+        return {'mac': ['leopard', 'snowleopard'], 'win': ['xp', 'vista', 'win7'], 'linux': ['lucid']}
+
+    def all_baseline_variants(self):
+        return self.ALL_BASELINE_VARIANTS
+
+    def virtual_test_suites(self):
+        return [
+            VirtualTestSuite('virtual/passes', 'passes', ['--virtual-arg']),
+            VirtualTestSuite('virtual/skipped', 'failures/expected', ['--virtual-arg2']),
+        ]
+
+
+class TestDriver(Driver):
+    """Test/Dummy implementation of the DumpRenderTree interface."""
+
+    def cmd_line(self, pixel_tests, per_test_args):
+        pixel_tests_flag = '-p' if pixel_tests else ''
+        return [self._port._path_to_driver()] + [pixel_tests_flag] + self._port.get_option('additional_drt_flag', []) + per_test_args
+
+    def run_test(self, test_input, stop_when_done):
+        start_time = time.time()
+        test_name = test_input.test_name
+        test_args = test_input.args or []
+        test = self._port._tests[test_name]
+        if test.keyboard:
+            raise KeyboardInterrupt
+        if test.exception:
+            raise ValueError('exception from ' + test_name)
+        if test.hang:
+            time.sleep((float(test_input.timeout) * 4) / 1000.0 + 1.0)  # The 1.0 comes from thread_padding_sec in layout_test_runnery.
+
+        audio = None
+        actual_text = test.actual_text
+
+        if 'flaky' in test_name and not test_name in self._port._flakes:
+            self._port._flakes.add(test_name)
+            actual_text = 'flaky text failure'
+
+        if actual_text and test_args and test_name == 'passes/args.html':
+            actual_text = actual_text + ' ' + ' '.join(test_args)
+
+        if test.actual_audio:
+            audio = base64.b64decode(test.actual_audio)
+        crashed_process_name = None
+        crashed_pid = None
+        if test.crash:
+            crashed_process_name = self._port.driver_name()
+            crashed_pid = 1
+        elif test.web_process_crash:
+            crashed_process_name = 'WebProcess'
+            crashed_pid = 2
+
+        crash_log = ''
+        if crashed_process_name:
+            crash_logs = CrashLogs(self._port.host)
+            crash_log = crash_logs.find_newest_log(crashed_process_name, None) or ''
+
+        if stop_when_done:
+            self.stop()
+
+        if test.actual_checksum == test_input.image_hash:
+            image = None
+        else:
+            image = test.actual_image
+        return DriverOutput(actual_text, image, test.actual_checksum, audio,
+            crash=test.crash or test.web_process_crash, crashed_process_name=crashed_process_name,
+            crashed_pid=crashed_pid, crash_log=crash_log,
+            test_time=time.time() - start_time, timeout=test.timeout, error=test.error)
+
+    def start(self, pixel_tests, per_test_args):
+        pass
+
+    def stop(self):
+        pass
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/win.py b/Tools/Scripts/webkitpy/layout_tests/port/win.py
new file mode 100644
index 0000000..ff473fe
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/win.py
@@ -0,0 +1,99 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import logging
+import re
+import sys
+
+from webkitpy.common.system.systemhost import SystemHost
+from webkitpy.common.system.executive import ScriptError, Executive
+from webkitpy.common.system.path import abspath_to_uri
+from webkitpy.layout_tests.port.apple import ApplePort
+
+
+_log = logging.getLogger(__name__)
+
+
+class WinPort(ApplePort):
+    port_name = "win"
+
+    VERSION_FALLBACK_ORDER = ["win-xp", "win-vista", "win-7sp0", "win-win7"]
+
+    ARCHITECTURES = ['x86']
+
+    def do_text_results_differ(self, expected_text, actual_text):
+        # Sanity was restored in WK2, so we don't need this hack there.
+        if self.get_option('webkit_test_runner'):
+            return ApplePort.do_text_results_differ(self, expected_text, actual_text)
+
+        # This is a hack (which dates back to ORWT).
+        # Windows does not have an EDITING DELEGATE, so we strip any EDITING DELEGATE
+        # messages to make more of the tests pass.
+        # It's possible more of the ports might want this and this could move down into WebKitPort.
+        delegate_regexp = re.compile("^EDITING DELEGATE: .*?\n", re.MULTILINE)
+        expected_text = delegate_regexp.sub("", expected_text)
+        actual_text = delegate_regexp.sub("", actual_text)
+        return expected_text != actual_text
+
+    def default_baseline_search_path(self):
+        if self._name.endswith(self.FUTURE_VERSION):
+            fallback_names = [self.port_name]
+        else:
+            fallback_names = self.VERSION_FALLBACK_ORDER[self.VERSION_FALLBACK_ORDER.index(self._name):-1] + [self.port_name]
+        # FIXME: The AppleWin port falls back to AppleMac for some results.  Eventually we'll have a shared 'apple' port.
+        if self.get_option('webkit_test_runner'):
+            fallback_names.insert(0, 'win-wk2')
+            fallback_names.append('mac-wk2')
+            # Note we do not add 'wk2' here, even though it's included in _skipped_search_paths().
+        # FIXME: Perhaps we should get this list from MacPort?
+        fallback_names.extend(['mac-lion', 'mac'])
+        return map(self._webkit_baseline_path, fallback_names)
+
+    def operating_system(self):
+        return 'win'
+
+    def show_results_html_file(self, results_filename):
+        self._run_script('run-safari', [abspath_to_uri(SystemHost().platform, results_filename)])
+
+    # FIXME: webkitperl/httpd.pm installs /usr/lib/apache/libphp4.dll on cycwin automatically
+    # as part of running old-run-webkit-tests.  That's bad design, but we may need some similar hack.
+    # We might use setup_environ_for_server for such a hack (or modify apache_http_server.py).
+
+    def _runtime_feature_list(self):
+        supported_features_command = [self._path_to_driver(), '--print-supported-features']
+        try:
+            output = self._executive.run_command(supported_features_command, error_handler=Executive.ignore_error)
+        except OSError, e:
+            _log.warn("Exception running driver: %s, %s.  Driver must be built before calling WebKitPort.test_expectations()." % (supported_features_command, e))
+            return None
+
+        # Note: win/DumpRenderTree.cpp does not print a leading space before the features_string.
+        match_object = re.match("SupportedFeatures:\s*(?P<features_string>.*)\s*", output)
+        if not match_object:
+            return None
+        return match_object.group('features_string').split(' ')
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/win_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/win_unittest.py
new file mode 100644
index 0000000..930dcd8
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/win_unittest.py
@@ -0,0 +1,109 @@
+# Copyright (C) 2011 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 StringIO
+import unittest
+
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.system.executive_mock import MockExecutive, MockExecutive2
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+from webkitpy.layout_tests.port import port_testcase
+from webkitpy.layout_tests.port.win import WinPort
+from webkitpy.tool.mocktool import MockOptions
+
+
+class WinPortTest(port_testcase.PortTestCase):
+    os_name = 'win'
+    os_version = 'xp'
+    port_name = 'win-xp'
+    port_maker = WinPort
+
+    def test_show_results_html_file(self):
+        port = self.make_port()
+        port._executive = MockExecutive(should_log=True)
+        capture = OutputCapture()
+        capture.capture_output()
+        port.show_results_html_file('test.html')
+        _, stderr, _ = capture.restore_output()
+        # We can't know for sure what path will be produced by cygpath, but we can assert about
+        # everything else.
+        self.assertTrue(stderr.startswith("MOCK run_command: ['Tools/Scripts/run-safari', '--release', '"))
+        self.assertTrue(stderr.endswith("test.html'], cwd=/mock-checkout\n"))
+
+    def _assert_search_path(self, expected_search_paths, version, use_webkit2=False):
+        port = self.make_port(port_name='win', os_version=version, options=MockOptions(webkit_test_runner=use_webkit2))
+        absolute_search_paths = map(port._webkit_baseline_path, expected_search_paths)
+        self.assertEquals(port.baseline_search_path(), absolute_search_paths)
+
+    def test_baseline_search_path(self):
+        self._assert_search_path(['win-xp', 'win-vista', 'win-7sp0', 'win', 'mac-lion', 'mac'], 'xp')
+        self._assert_search_path(['win-vista', 'win-7sp0', 'win', 'mac-lion', 'mac'], 'vista')
+        self._assert_search_path(['win-7sp0', 'win', 'mac-lion', 'mac'], '7sp0')
+
+        self._assert_search_path(['win-wk2', 'win-xp', 'win-vista', 'win-7sp0', 'win', 'mac-wk2', 'mac-lion', 'mac'], 'xp', use_webkit2=True)
+        self._assert_search_path(['win-wk2', 'win-vista', 'win-7sp0', 'win', 'mac-wk2', 'mac-lion', 'mac'], 'vista', use_webkit2=True)
+        self._assert_search_path(['win-wk2', 'win-7sp0', 'win', 'mac-wk2', 'mac-lion', 'mac'], '7sp0', use_webkit2=True)
+
+    def _assert_version(self, port_name, expected_version):
+        host = MockSystemHost(os_name='win', os_version=expected_version)
+        port = WinPort(host, port_name=port_name)
+        self.assertEquals(port.version(), expected_version)
+
+    def test_versions(self):
+        self._assert_version('win-xp', 'xp')
+        self._assert_version('win-vista', 'vista')
+        self._assert_version('win-7sp0', '7sp0')
+        self.assertRaises(AssertionError, self._assert_version, 'win-me', 'xp')
+
+    def test_compare_text(self):
+        expected = "EDITING DELEGATE: webViewDidChangeSelection:WebViewDidChangeSelectionNotification\nfoo\nEDITING DELEGATE: webViewDidChangeSelection:WebViewDidChangeSelectionNotification\n"
+        port = self.make_port()
+        self.assertFalse(port.do_text_results_differ(expected, "foo\n"))
+        self.assertTrue(port.do_text_results_differ(expected, "foo"))
+        self.assertTrue(port.do_text_results_differ(expected, "bar"))
+
+        # This hack doesn't exist in WK2.
+        port._options = MockOptions(webkit_test_runner=True)
+        self.assertTrue(port.do_text_results_differ(expected, "foo\n"))
+
+    def test_operating_system(self):
+        self.assertEqual('win', self.make_port().operating_system())
+
+    def test_runtime_feature_list(self):
+        port = self.make_port()
+        port._executive.run_command = lambda command, cwd=None, error_handler=None: "Nonsense"
+        # runtime_features_list returns None when its results are meaningless (it couldn't run DRT or parse the output, etc.)
+        self.assertEquals(port._runtime_feature_list(), None)
+        port._executive.run_command = lambda command, cwd=None, error_handler=None: "SupportedFeatures:foo bar"
+        self.assertEquals(port._runtime_feature_list(), ['foo', 'bar'])
+
+    def test_expectations_files(self):
+        self.assertEquals(len(self.make_port().expectations_files()), 2)
+        self.assertEquals(len(self.make_port(options=MockOptions(webkit_test_runner=True)).expectations_files()), 4)
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/xvfbdriver.py b/Tools/Scripts/webkitpy/layout_tests/port/xvfbdriver.py
new file mode 100644
index 0000000..b98c039
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/xvfbdriver.py
@@ -0,0 +1,104 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import logging
+import os
+import re
+import time
+
+from webkitpy.layout_tests.port.server_process import ServerProcess
+from webkitpy.layout_tests.port.driver import Driver
+from webkitpy.common.system.file_lock import FileLock
+
+_log = logging.getLogger(__name__)
+
+
+class XvfbDriver(Driver):
+    def __init__(self, *args, **kwargs):
+        Driver.__init__(self, *args, **kwargs)
+        self._guard_lock = None
+        self._startup_delay_secs = 1.0
+
+    def _next_free_display(self):
+        running_pids = self._port.host.executive.run_command(['ps', '-eo', 'comm,command'])
+        reserved_screens = set()
+        for pid in running_pids.split('\n'):
+            match = re.match('(X|Xvfb|Xorg)\s+.*\s:(?P<screen_number>\d+)', pid)
+            if match:
+                reserved_screens.add(int(match.group('screen_number')))
+        for i in range(99):
+            if i not in reserved_screens:
+                _guard_lock_file = self._port.host.filesystem.join('/tmp', 'WebKitXvfb.lock.%i' % i)
+                self._guard_lock = FileLock(_guard_lock_file)
+                if self._guard_lock.acquire_lock():
+                    return i
+
+    def _start(self, pixel_tests, per_test_args):
+        # Use even displays for pixel tests and odd ones otherwise. When pixel tests are disabled,
+        # DriverProxy creates two drivers, one for normal and the other for ref tests. Both have
+        # the same worker number, so this prevents them from using the same Xvfb instance.
+        display_id = self._next_free_display()
+        self._lock_file = "/tmp/.X%d-lock" % display_id
+
+        run_xvfb = ["Xvfb", ":%d" % display_id, "-screen",  "0", "800x600x24", "-nolisten", "tcp"]
+        with open(os.devnull, 'w') as devnull:
+            self._xvfb_process = self._port.host.executive.popen(run_xvfb, stderr=devnull)
+
+        # Crashes intend to occur occasionally in the first few tests that are run through each
+        # worker because the Xvfb display isn't ready yet. Halting execution a bit should avoid that.
+        time.sleep(self._startup_delay_secs)
+
+        server_name = self._port.driver_name()
+        environment = self._port.setup_environ_for_server(server_name)
+        # We must do this here because the DISPLAY number depends on _worker_number
+        environment['DISPLAY'] = ":%d" % display_id
+        self._driver_tempdir = self._port._filesystem.mkdtemp(prefix='%s-' % self._port.driver_name())
+        environment['DUMPRENDERTREE_TEMP'] = str(self._driver_tempdir)
+        environment['LOCAL_RESOURCE_ROOT'] = self._port.layout_tests_dir()
+
+        # Currently on WebKit2, there is no API for setting the application
+        # cache directory. Each worker should have it's own and it should be
+        # cleaned afterwards, so we set it to inside the temporary folder by
+        # prepending XDG_CACHE_HOME with DUMPRENDERTREE_TEMP.
+        environment['XDG_CACHE_HOME'] = self._port.host.filesystem.join(str(self._driver_tempdir), 'appcache')
+
+        self._crashed_process_name = None
+        self._crashed_pid = None
+        self._server_process = self._port._server_process_constructor(self._port, server_name, self.cmd_line(pixel_tests, per_test_args), environment)
+        self._server_process.start()
+
+    def stop(self):
+        super(XvfbDriver, self).stop()
+        if self._guard_lock:
+            self._guard_lock.release_lock()
+            self._guard_lock = None
+        if getattr(self, '_xvfb_process', None):
+            self._port.host.executive.kill_process(self._xvfb_process.pid)
+            self._xvfb_process = None
+            if self._port.host.filesystem.exists(self._lock_file):
+                self._port.host.filesystem.remove(self._lock_file)
diff --git a/Tools/Scripts/webkitpy/layout_tests/port/xvfbdriver_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/xvfbdriver_unittest.py
new file mode 100644
index 0000000..220dd35
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/port/xvfbdriver_unittest.py
@@ -0,0 +1,132 @@
+# Copyright (C) 2012 Zan Dobersek <zandobersek@gmail.com>
+#
+# 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.system.deprecated_logging import log
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.executive_mock import MockExecutive2
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+from webkitpy.layout_tests.port import Port
+from webkitpy.layout_tests.port.config_mock import MockConfig
+from webkitpy.layout_tests.port.server_process_mock import MockServerProcess
+from webkitpy.layout_tests.port.xvfbdriver import XvfbDriver
+
+
+class XvfbDriverTest(unittest.TestCase):
+    def make_driver(self, worker_number=0, xorg_running=False, executive=None):
+        port = Port(host=MockSystemHost(log_executive=True, executive=executive), config=MockConfig())
+        port._server_process_constructor = MockServerProcess
+        if xorg_running:
+            port._executive._running_pids['Xorg'] = 108
+
+        driver = XvfbDriver(port, worker_number=worker_number, pixel_tests=True)
+        driver._startup_delay_secs = 0
+        return driver
+
+    def cleanup_driver(self, driver):
+        # Setting _xvfb_process member to None is necessary as the Driver object is stopped on deletion,
+        # killing the Xvfb process if present. Thus, this method should only be called from tests that do not
+        # intend to test the behavior of XvfbDriver.stop.
+        driver._xvfb_process = None
+
+    def assertDriverStartSuccessful(self, driver, expected_stderr, expected_display, pixel_tests=False):
+        OutputCapture().assert_outputs(self, driver.start, [pixel_tests, []], expected_stderr=expected_stderr)
+        self.assertTrue(driver._server_process.started)
+        self.assertEqual(driver._server_process.env["DISPLAY"], expected_display)
+
+    def test_start_no_pixel_tests(self):
+        driver = self.make_driver()
+        expected_stderr = "MOCK run_command: ['ps', '-eo', 'comm,command'], cwd=None\nMOCK popen: ['Xvfb', ':0', '-screen', '0', '800x600x24', '-nolisten', 'tcp']\n"
+        self.assertDriverStartSuccessful(driver, expected_stderr=expected_stderr, expected_display=":0")
+        self.cleanup_driver(driver)
+
+    def test_start_pixel_tests(self):
+        driver = self.make_driver()
+        expected_stderr = "MOCK run_command: ['ps', '-eo', 'comm,command'], cwd=None\nMOCK popen: ['Xvfb', ':0', '-screen', '0', '800x600x24', '-nolisten', 'tcp']\n"
+        self.assertDriverStartSuccessful(driver, expected_stderr=expected_stderr, expected_display=":0", pixel_tests=True)
+        self.cleanup_driver(driver)
+
+    def test_start_arbitrary_worker_number(self):
+        driver = self.make_driver(worker_number=17)
+        expected_stderr = "MOCK run_command: ['ps', '-eo', 'comm,command'], cwd=None\nMOCK popen: ['Xvfb', ':0', '-screen', '0', '800x600x24', '-nolisten', 'tcp']\n"
+        self.assertDriverStartSuccessful(driver, expected_stderr=expected_stderr, expected_display=":0", pixel_tests=True)
+        self.cleanup_driver(driver)
+
+    def disabled_test_next_free_display(self):
+        output = "Xorg            /usr/bin/X :0 -auth /var/run/lightdm/root/:0 -nolisten tcp vt7 -novtswitch -background none\nXvfb            Xvfb :1 -screen 0 800x600x24 -nolisten tcp"
+        executive = MockExecutive2(output)
+        driver = self.make_driver(executive=executive)
+        self.assertEqual(driver._next_free_display(), 2)
+        self.cleanup_driver(driver)
+        output = "X               /usr/bin/X :0 vt7 -nolisten tcp -auth /var/run/xauth/A:0-8p7Ybb"
+        executive = MockExecutive2(output)
+        driver = self.make_driver(executive=executive)
+        self.assertEqual(driver._next_free_display(), 1)
+        self.cleanup_driver(driver)
+        output = "Xvfb            Xvfb :0 -screen 0 800x600x24 -nolisten tcp"
+        executive = MockExecutive2(output)
+        driver = self.make_driver(executive=executive)
+        self.assertEqual(driver._next_free_display(), 1)
+        self.cleanup_driver(driver)
+        output = "Xvfb            Xvfb :1 -screen 0 800x600x24 -nolisten tcp\nXvfb            Xvfb :0 -screen 0 800x600x24 -nolisten tcp\nXvfb            Xvfb :3 -screen 0 800x600x24 -nolisten tcp"
+        executive = MockExecutive2(output)
+        driver = self.make_driver(executive=executive)
+        self.assertEqual(driver._next_free_display(), 2)
+        self.cleanup_driver(driver)
+
+    def test_start_next_worker(self):
+        driver = self.make_driver()
+        driver._next_free_display = lambda: 0
+        expected_stderr = "MOCK popen: ['Xvfb', ':0', '-screen', '0', '800x600x24', '-nolisten', 'tcp']\n"
+        self.assertDriverStartSuccessful(driver, expected_stderr=expected_stderr, expected_display=":0", pixel_tests=True)
+        self.cleanup_driver(driver)
+        driver = self.make_driver()
+        driver._next_free_display = lambda: 3
+        expected_stderr = "MOCK popen: ['Xvfb', ':3', '-screen', '0', '800x600x24', '-nolisten', 'tcp']\n"
+        self.assertDriverStartSuccessful(driver, expected_stderr=expected_stderr, expected_display=":3", pixel_tests=True)
+        self.cleanup_driver(driver)
+
+    def test_stop(self):
+        filesystem = MockFileSystem(files={'/tmp/.X42-lock': '1234\n'})
+        port = Port(host=MockSystemHost(log_executive=True, filesystem=filesystem), config=MockConfig())
+        port._executive.kill_process = lambda x: log("MOCK kill_process pid: " + str(x))
+        driver = XvfbDriver(port, worker_number=0, pixel_tests=True)
+
+        class FakeXvfbProcess(object):
+            pid = 1234
+
+        driver._xvfb_process = FakeXvfbProcess()
+        driver._lock_file = '/tmp/.X42-lock'
+
+        expected_stderr = "MOCK kill_process pid: 1234\n"
+        OutputCapture().assert_outputs(self, driver.stop, [], expected_stderr=expected_stderr)
+
+        self.assertEqual(driver._xvfb_process, None)
+        self.assertFalse(port._filesystem.exists(driver._lock_file))
diff --git a/Tools/Scripts/webkitpy/layout_tests/reftests/__init__.py b/Tools/Scripts/webkitpy/layout_tests/reftests/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/reftests/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/layout_tests/reftests/extract_reference_link.py b/Tools/Scripts/webkitpy/layout_tests/reftests/extract_reference_link.py
new file mode 100644
index 0000000..e21d73d
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/reftests/extract_reference_link.py
@@ -0,0 +1,63 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Utility module for reftests."""
+
+
+from HTMLParser import HTMLParser
+
+
+class ExtractReferenceLinkParser(HTMLParser):
+
+    def __init__(self):
+        HTMLParser.__init__(self)
+        self.matches = []
+        self.mismatches = []
+
+    def handle_starttag(self, tag, attrs):
+        if tag != "link":
+            return
+        attrs = dict(attrs)
+        if not "rel" in attrs:
+            return
+        if not "href" in attrs:
+            return
+        if attrs["rel"] == "match":
+            self.matches.append(attrs["href"])
+        if attrs["rel"] == "mismatch":
+            self.mismatches.append(attrs["href"])
+
+
+def get_reference_link(html_string):
+    """Returns reference links in the given html_string.
+
+    Returns:
+        a tuple of two URL lists, (matches, mismatches).
+    """
+    parser = ExtractReferenceLinkParser()
+    parser.feed(html_string)
+    parser.close()
+
+    return parser.matches, parser.mismatches
diff --git a/Tools/Scripts/webkitpy/layout_tests/reftests/extract_reference_link_unittest.py b/Tools/Scripts/webkitpy/layout_tests/reftests/extract_reference_link_unittest.py
new file mode 100644
index 0000000..717bc7c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/reftests/extract_reference_link_unittest.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.layout_tests.reftests import extract_reference_link
+
+
+class ExtractLinkMatchTest(unittest.TestCase):
+
+    def test_getExtractMatch(self):
+        html_1 = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>CSS Test: DESCRIPTION OF TEST</title>
+<link rel="author" title="NAME_OF_AUTHOR"
+href="mailto:EMAIL OR http://CONTACT_PAGE"/>
+<link rel="help" href="RELEVANT_SPEC_SECTION"/>
+<link rel="match" href="green-box-ref.xht" />
+<link rel="match" href="blue-box-ref.xht" />
+<link rel="mismatch" href="red-box-notref.xht" />
+<link rel="mismatch" href="red-box-notref.xht" />
+<meta name="flags" content="TOKENS" />
+<meta name="assert" content="TEST ASSERTION"/>
+<style type="text/css"><![CDATA[
+CSS FOR TEST
+]]></style>
+</head>
+<body>
+CONTENT OF TEST
+</body>
+</html>
+"""
+        matches, mismatches = extract_reference_link.get_reference_link(html_1)
+        self.assertEqual(matches,
+                         ["green-box-ref.xht", "blue-box-ref.xht"])
+        self.assertEqual(mismatches,
+                         ["red-box-notref.xht", "red-box-notref.xht"])
+
+        html_2 = ""
+        empty_tuple_1 = extract_reference_link.get_reference_link(html_2)
+        self.assertEqual(empty_tuple_1, ([], []))
+
+        # Link does not have a "ref" attribute.
+        html_3 = """<link href="RELEVANT_SPEC_SECTION"/>"""
+        empty_tuple_2 = extract_reference_link.get_reference_link(html_3)
+        self.assertEqual(empty_tuple_2, ([], []))
+
+        # Link does not have a "href" attribute.
+        html_4 = """<link rel="match"/>"""
+        empty_tuple_3 = extract_reference_link.get_reference_link(html_4)
+        self.assertEqual(empty_tuple_3, ([], []))
+
+        # Link does not have a "/" at the end.
+        html_5 = """<link rel="help" href="RELEVANT_SPEC_SECTION">"""
+        empty_tuple_4 = extract_reference_link.get_reference_link(html_5)
+        self.assertEqual(empty_tuple_4, ([], []))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests.py b/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests.py
new file mode 100755
index 0000000..1c8e732
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests.py
@@ -0,0 +1,495 @@
+#!/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
+# Copyright (C) 2011 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:
+#
+#     * 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 errno
+import logging
+import optparse
+import os
+import signal
+import sys
+import traceback
+
+from webkitpy.common.host import Host
+from webkitpy.common.system import stack_utils
+from webkitpy.layout_tests.controllers.manager import Manager, WorkerException, TestRunInterruptedException
+from webkitpy.layout_tests.models import test_expectations
+from webkitpy.layout_tests.port import configuration_options, platform_options
+from webkitpy.layout_tests.views import printing
+
+
+_log = logging.getLogger(__name__)
+
+
+# This mirrors what the shell normally does.
+INTERRUPTED_EXIT_STATUS = signal.SIGINT + 128
+
+# This is a randomly chosen exit code that can be tested against to
+# indicate that an unexpected exception occurred.
+EXCEPTIONAL_EXIT_STATUS = 254
+
+
+def lint(port, options):
+    host = port.host
+    if options.platform:
+        ports_to_lint = [port]
+    else:
+        ports_to_lint = [host.port_factory.get(name) for name in host.port_factory.all_port_names()]
+
+    files_linted = set()
+    lint_failed = False
+
+    for port_to_lint in ports_to_lint:
+        expectations_dict = port_to_lint.expectations_dict()
+        for expectations_file in expectations_dict.keys():
+            if expectations_file in files_linted:
+                continue
+
+            try:
+                test_expectations.TestExpectations(port_to_lint, expectations_to_lint={expectations_file: expectations_dict[expectations_file]})
+            except test_expectations.ParseError, e:
+                lint_failed = True
+                _log.error('')
+                for warning in e.warnings:
+                    _log.error(warning)
+                _log.error('')
+            files_linted.add(expectations_file)
+
+    if lint_failed:
+        _log.error('Lint failed.')
+        return -1
+    _log.info('Lint succeeded.')
+    return 0
+
+
+def run(port, options, args, regular_output=sys.stderr, buildbot_output=sys.stdout):
+    try:
+        warnings = _set_up_derived_options(port, options)
+
+        printer = printing.Printer(port, options, regular_output, buildbot_output, logger=logging.getLogger())
+
+        for warning in warnings:
+            _log.warning(warning)
+
+        if options.lint_test_files:
+            return lint(port, options)
+
+        # We wrap any parts of the run that are slow or likely to raise exceptions
+        # in a try/finally to ensure that we clean up the logging configuration.
+        unexpected_result_count = -1
+
+        manager = Manager(port, options, printer)
+        printer.print_config(port.results_directory())
+
+        unexpected_result_count = manager.run(args)
+        _log.debug("Testing completed, Exit status: %d" % unexpected_result_count)
+    except Exception:
+        exception_type, exception_value, exception_traceback = sys.exc_info()
+        if exception_type not in (KeyboardInterrupt, TestRunInterruptedException, WorkerException):
+            print >> sys.stderr, '\n%s raised: %s' % (exception_type.__name__, exception_value)
+            stack_utils.log_traceback(_log.error, exception_traceback)
+        raise
+    finally:
+        printer.cleanup()
+
+    return unexpected_result_count
+
+
+def _set_up_derived_options(port, options):
+    """Sets the options values that depend on other options values."""
+    # We return a list of warnings to print after the printer is initialized.
+    warnings = []
+
+    if not options.child_processes:
+        options.child_processes = os.environ.get("WEBKIT_TEST_CHILD_PROCESSES",
+                                                 str(port.default_child_processes()))
+    if not options.max_locked_shards:
+        options.max_locked_shards = int(os.environ.get("WEBKIT_TEST_MAX_LOCKED_SHARDS",
+                                                       str(port.default_max_locked_shards())))
+
+    if not options.configuration:
+        options.configuration = port.default_configuration()
+
+    if options.pixel_tests is None:
+        options.pixel_tests = port.default_pixel_tests()
+
+    if not options.time_out_ms:
+        options.time_out_ms = str(port.default_timeout_ms())
+
+    options.slow_time_out_ms = str(5 * int(options.time_out_ms))
+
+    if options.additional_platform_directory:
+        additional_platform_directories = []
+        for path in options.additional_platform_directory:
+            additional_platform_directories.append(port.host.filesystem.abspath(path))
+        options.additional_platform_directory = additional_platform_directories
+
+    if not options.http and options.skipped in ('ignore', 'only'):
+        warnings.append("--force/--skipped=%s overrides --no-http." % (options.skipped))
+        options.http = True
+
+    if options.ignore_metrics and (options.new_baseline or options.reset_results):
+        warnings.append("--ignore-metrics has no effect with --new-baselines or with --reset-results")
+
+    if options.new_baseline:
+        options.reset_results = True
+        options.add_platform_exceptions = True
+
+    if options.pixel_test_directories:
+        options.pixel_tests = True
+        varified_dirs = set()
+        pixel_test_directories = options.pixel_test_directories
+        for directory in pixel_test_directories:
+            # FIXME: we should support specifying the directories all the ways we support it for additional
+            # arguments specifying which tests and directories to run. We should also move the logic for that
+            # to Port.
+            filesystem = port.host.filesystem
+            if not filesystem.isdir(filesystem.join(port.layout_tests_dir(), directory)):
+                warnings.append("'%s' was passed to --pixel-test-directories, which doesn't seem to be a directory" % str(directory))
+            else:
+                varified_dirs.add(directory)
+
+        options.pixel_test_directories = list(varified_dirs)
+
+    if options.run_singly:
+        options.verbose = True
+
+    return warnings
+
+
+def _compat_shim_callback(option, opt_str, value, parser):
+    print "Ignoring unsupported option: %s" % opt_str
+
+
+def _compat_shim_option(option_name, **kwargs):
+    return optparse.make_option(option_name, action="callback",
+        callback=_compat_shim_callback,
+        help="Ignored, for old-run-webkit-tests compat only.", **kwargs)
+
+
+def parse_args(args=None):
+    """Provides a default set of command line args.
+
+    Returns a tuple of options, args from optparse"""
+
+    option_group_definitions = []
+
+    option_group_definitions.append(("Platform options", platform_options()))
+    option_group_definitions.append(("Configuration options", configuration_options()))
+    option_group_definitions.append(("Printing Options", printing.print_options()))
+
+    # FIXME: These options should move onto the ChromiumPort.
+    option_group_definitions.append(("Chromium-specific Options", [
+        optparse.make_option("--startup-dialog", action="store_true",
+            default=False, help="create a dialog on DumpRenderTree startup"),
+        optparse.make_option("--gp-fault-error-box", action="store_true",
+            default=False, help="enable Windows GP fault error box"),
+        optparse.make_option("--js-flags",
+            type="string", help="JavaScript flags to pass to tests"),
+        optparse.make_option("--stress-opt", action="store_true",
+            default=False,
+            help="Enable additional stress test to JavaScript optimization"),
+        optparse.make_option("--stress-deopt", action="store_true",
+            default=False,
+            help="Enable additional stress test to JavaScript optimization"),
+        optparse.make_option("--nocheck-sys-deps", action="store_true",
+            default=False,
+            help="Don't check the system dependencies (themes)"),
+        optparse.make_option("--accelerated-video",
+            action="store_true",
+            help="Use hardware-accelerated compositing for video"),
+        optparse.make_option("--no-accelerated-video",
+            action="store_false",
+            dest="accelerated_video",
+            help="Don't use hardware-accelerated compositing for video"),
+        optparse.make_option("--threaded-compositing",
+            action="store_true",
+            help="Use threaded compositing for rendering"),
+        optparse.make_option("--accelerated-2d-canvas",
+            action="store_true",
+            help="Use hardware-accelerated 2D Canvas calls"),
+        optparse.make_option("--no-accelerated-2d-canvas",
+            action="store_false",
+            dest="accelerated_2d_canvas",
+            help="Don't use hardware-accelerated 2D Canvas calls"),
+        optparse.make_option("--accelerated-painting",
+            action="store_true",
+            default=False,
+            help="Use hardware accelerated painting of composited pages"),
+        optparse.make_option("--per-tile-painting",
+            action="store_true",
+            help="Use per-tile painting of composited pages"),
+        optparse.make_option("--adb-device",
+            action="append", default=[],
+            help="Run Android layout tests on these devices."),
+    ]))
+
+    option_group_definitions.append(("EFL-specific Options", [
+        optparse.make_option("--webprocess-cmd-prefix", type="string",
+            default=False, help="Prefix used when spawning the Web process (Debug mode only)"),
+    ]))
+
+    option_group_definitions.append(("WebKit Options", [
+        optparse.make_option("--gc-between-tests", action="store_true", default=False,
+            help="Force garbage collection between each test"),
+        optparse.make_option("--complex-text", action="store_true", default=False,
+            help="Use the complex text code path for all text (Mac OS X and Windows only)"),
+        optparse.make_option("-l", "--leaks", action="store_true", default=False,
+            help="Enable leaks checking (Mac OS X only)"),
+        optparse.make_option("-g", "--guard-malloc", action="store_true", default=False,
+            help="Enable Guard Malloc (Mac OS X only)"),
+        optparse.make_option("--threaded", action="store_true", default=False,
+            help="Run a concurrent JavaScript thread with each test"),
+        optparse.make_option("--webkit-test-runner", "-2", action="store_true",
+            help="Use WebKitTestRunner rather than DumpRenderTree."),
+        # FIXME: We should merge this w/ --build-directory and only have one flag.
+        optparse.make_option("--root", action="store",
+            help="Path to a directory containing the executables needed to run tests."),
+    ]))
+
+    option_group_definitions.append(("ORWT Compatibility Options", [
+        # FIXME: Remove this option once the bots don't refer to it.
+        # results.html is smart enough to figure this out itself.
+        _compat_shim_option("--use-remote-links-to-tests"),
+    ]))
+
+    option_group_definitions.append(("Results Options", [
+        optparse.make_option("-p", "--pixel-tests", action="store_true",
+            dest="pixel_tests", help="Enable pixel-to-pixel PNG comparisons"),
+        optparse.make_option("--no-pixel-tests", action="store_false",
+            dest="pixel_tests", help="Disable pixel-to-pixel PNG comparisons"),
+        optparse.make_option("--no-sample-on-timeout", action="store_false",
+            dest="sample_on_timeout", help="Don't run sample on timeout (Mac OS X only)"),
+        optparse.make_option("--no-ref-tests", action="store_true",
+            dest="no_ref_tests", help="Skip all ref tests"),
+        optparse.make_option("--tolerance",
+            help="Ignore image differences less than this percentage (some "
+                "ports may ignore this option)", type="float"),
+        optparse.make_option("--results-directory", help="Location of test results"),
+        optparse.make_option("--build-directory",
+            help="Path to the directory under which build files are kept (should not include configuration)"),
+        optparse.make_option("--add-platform-exceptions", action="store_true", default=False,
+            help="Save generated results into the *most-specific-platform* directory rather than the *generic-platform* directory"),
+        optparse.make_option("--new-baseline", action="store_true",
+            default=False, help="Save generated results as new baselines "
+                 "into the *most-specific-platform* directory, overwriting whatever's "
+                 "already there. Equivalent to --reset-results --add-platform-exceptions"),
+        optparse.make_option("--reset-results", action="store_true",
+            default=False, help="Reset expectations to the "
+                 "generated results in their existing location."),
+        optparse.make_option("--no-new-test-results", action="store_false",
+            dest="new_test_results", default=True,
+            help="Don't create new baselines when no expected results exist"),
+
+        #FIXME: we should support a comma separated list with --pixel-test-directory as well.
+        optparse.make_option("--pixel-test-directory", action="append", default=[], dest="pixel_test_directories",
+            help="A directory where it is allowed to execute tests as pixel tests. "
+                 "Specify multiple times to add multiple directories. "
+                 "This option implies --pixel-tests. If specified, only those tests "
+                 "will be executed as pixel tests that are located in one of the "
+                 "directories enumerated with the option. Some ports may ignore this "
+                 "option while others can have a default value that can be overridden here."),
+
+        optparse.make_option("--skip-failing-tests", action="store_true",
+            default=False, help="Skip tests that are expected to fail. "
+                 "Note: When using this option, you might miss new crashes "
+                 "in these tests."),
+        optparse.make_option("--additional-drt-flag", action="append",
+            default=[], help="Additional command line flag to pass to DumpRenderTree "
+                 "Specify multiple times to add multiple flags."),
+        optparse.make_option("--driver-name", type="string",
+            help="Alternative DumpRenderTree binary to use"),
+        optparse.make_option("--additional-platform-directory", action="append",
+            default=[], help="Additional directory where to look for test "
+                 "baselines (will take precendence over platform baselines). "
+                 "Specify multiple times to add multiple search path entries."),
+        optparse.make_option("--additional-expectations", action="append", default=[],
+            help="Path to a test_expectations file that will override previous expectations. "
+                 "Specify multiple times for multiple sets of overrides."),
+        optparse.make_option("--compare-port", action="store", default=None,
+            help="Use the specified port's baselines first"),
+        optparse.make_option("--no-show-results", action="store_false",
+            default=True, dest="show_results",
+            help="Don't launch a browser with results after the tests "
+                 "are done"),
+        # FIXME: We should have a helper function to do this sort of
+        # deprectated mapping and automatically log, etc.
+        optparse.make_option("--noshow-results", action="store_false", dest="show_results", help="Deprecated, same as --no-show-results."),
+        optparse.make_option("--no-launch-safari", action="store_false", dest="show_results", help="Deprecated, same as --no-show-results."),
+        optparse.make_option("--full-results-html", action="store_true",
+            default=False,
+            help="Show all failures in results.html, rather than only regressions"),
+        optparse.make_option("--clobber-old-results", action="store_true",
+            default=False, help="Clobbers test results from previous runs."),
+        optparse.make_option("--no-record-results", action="store_false",
+            default=True, dest="record_results",
+            help="Don't record the results."),
+        optparse.make_option("--http", action="store_true", dest="http",
+            default=True, help="Run HTTP and WebSocket tests (default)"),
+        optparse.make_option("--no-http", action="store_false", dest="http",
+            help="Don't run HTTP and WebSocket tests"),
+        optparse.make_option("--ignore-metrics", action="store_true", dest="ignore_metrics",
+            default=False, help="Ignore rendering metrics related information from test "
+            "output, only compare the structure of the rendertree."),
+    ]))
+
+    option_group_definitions.append(("Testing Options", [
+        optparse.make_option("--build", dest="build",
+            action="store_true", default=True,
+            help="Check to ensure the DumpRenderTree build is up-to-date "
+                 "(default)."),
+        optparse.make_option("--no-build", dest="build",
+            action="store_false", help="Don't check to see if the "
+                                       "DumpRenderTree build is up-to-date."),
+        optparse.make_option("-n", "--dry-run", action="store_true",
+            default=False,
+            help="Do everything but actually run the tests or upload results."),
+        optparse.make_option("--wrapper",
+            help="wrapper command to insert before invocations of "
+                 "DumpRenderTree; option is split on whitespace before "
+                 "running. (Example: --wrapper='valgrind --smc-check=all')"),
+        optparse.make_option("-i", "--ignore-tests", action="append", default=[],
+            help="directories or test to ignore (may specify multiple times)"),
+        optparse.make_option("--test-list", action="append",
+            help="read list of tests to run from file", metavar="FILE"),
+        optparse.make_option("--skipped", action="store", default="default",
+            help=("control how tests marked SKIP are run. "
+                 "'default' == Skip tests unless explicitly listed on the command line, "
+                 "'ignore' == Run them anyway, "
+                 "'only' == only run the SKIP tests, "
+                 "'always' == always skip, even if listed on the command line.")),
+        optparse.make_option("--force", dest="skipped", action="store_const", const='ignore',
+            help="Run all tests, even those marked SKIP in the test list (same as --skipped=ignore)"),
+        optparse.make_option("--time-out-ms",
+            help="Set the timeout for each test"),
+        optparse.make_option("--randomize-order", action="store_true",
+            default=False, help=("Run tests in random order (useful "
+                                "for tracking down corruption)")),
+        optparse.make_option("--run-chunk",
+            help=("Run a specified chunk (n:l), the nth of len l, "
+                 "of the layout tests")),
+        optparse.make_option("--run-part", help=("Run a specified part (n:m), "
+                  "the nth of m parts, of the layout tests")),
+        optparse.make_option("--batch-size",
+            help=("Run a the tests in batches (n), after every n tests, "
+                  "DumpRenderTree is relaunched."), type="int", default=None),
+        optparse.make_option("--run-singly", action="store_true",
+            default=False, help="run a separate DumpRenderTree for each test (implies --verbose)"),
+        optparse.make_option("--child-processes",
+            help="Number of DumpRenderTrees to run in parallel."),
+        # FIXME: Display default number of child processes that will run.
+        optparse.make_option("-f", "--fully-parallel", action="store_true",
+            help="run all tests in parallel"),
+        optparse.make_option("--exit-after-n-failures", type="int", default=None,
+            help="Exit after the first N failures instead of running all "
+            "tests"),
+        optparse.make_option("--exit-after-n-crashes-or-timeouts", type="int",
+            default=None, help="Exit after the first N crashes instead of "
+            "running all tests"),
+        optparse.make_option("--iterations", type="int", default=1, help="Number of times to run the set of tests (e.g. ABCABCABC)"),
+        optparse.make_option("--repeat-each", type="int", default=1, help="Number of times to run each test (e.g. AAABBBCCC)"),
+        optparse.make_option("--retry-failures", action="store_true",
+            default=True,
+            help="Re-try any tests that produce unexpected results (default)"),
+        optparse.make_option("--no-retry-failures", action="store_false",
+            dest="retry_failures",
+            help="Don't re-try any tests that produce unexpected results."),
+        optparse.make_option("--max-locked-shards", type="int", default=0,
+            help="Set the maximum number of locked shards"),
+        optparse.make_option("--additional-env-var", type="string", action="append", default=[],
+            help="Passes that environment variable to the tests (--additional-env-var=NAME=VALUE)"),
+    ]))
+
+    option_group_definitions.append(("Miscellaneous Options", [
+        optparse.make_option("--lint-test-files", action="store_true",
+        default=False, help=("Makes sure the test files parse for all "
+                            "configurations. Does not run any tests.")),
+    ]))
+
+    # FIXME: Move these into json_results_generator.py
+    option_group_definitions.append(("Result JSON Options", [
+        optparse.make_option("--master-name", help="The name of the buildbot master."),
+        optparse.make_option("--builder-name", default="",
+            help=("The name of the builder shown on the waterfall running "
+                  "this script e.g. WebKit.")),
+        optparse.make_option("--build-name", default="DUMMY_BUILD_NAME",
+            help=("The name of the builder used in its path, e.g. "
+                  "webkit-rel.")),
+        optparse.make_option("--build-number", default="DUMMY_BUILD_NUMBER",
+            help=("The build number of the builder running this script.")),
+        optparse.make_option("--test-results-server", default="",
+            help=("If specified, upload results json files to this appengine "
+                  "server.")),
+    ]))
+
+    option_parser = optparse.OptionParser()
+
+    for group_name, group_options in option_group_definitions:
+        option_group = optparse.OptionGroup(option_parser, group_name)
+        option_group.add_options(group_options)
+        option_parser.add_option_group(option_group)
+
+    return option_parser.parse_args(args)
+
+
+def main(argv=None):
+    try:
+        options, args = parse_args(argv)
+        if options.platform and 'test' in options.platform:
+            # It's a bit lame to import mocks into real code, but this allows the user
+            # to run tests against the test platform interactively, which is useful for
+            # debugging test failures.
+            from webkitpy.common.host_mock import MockHost
+            host = MockHost()
+        else:
+            host = Host()
+        port = host.port_factory.get(options.platform, options)
+    except NotImplementedError, e:
+        # FIXME: is this the best way to handle unsupported port names?
+        print >> sys.stderr, str(e)
+        return EXCEPTIONAL_EXIT_STATUS
+    except Exception, e:
+        print >> sys.stderr, '\n%s raised: %s' % (e.__class__.__name__, str(e))
+        traceback.print_exc(file=sys.stderr)
+        raise
+
+    logging.getLogger().setLevel(logging.DEBUG if options.debug_rwt_logging else logging.INFO)
+    return run(port, options, args)
+
+
+if '__main__' == __name__:
+    try:
+        return_code = main()
+    except BaseException, e:
+        if e.__class__ in (KeyboardInterrupt, TestRunInterruptedException):
+            sys.exit(INTERRUPTED_EXIT_STATUS)
+        sys.exit(EXCEPTIONAL_EXIT_STATUS)
+
+    sys.exit(return_code)
diff --git a/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests_integrationtest.py b/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests_integrationtest.py
new file mode 100755
index 0000000..8e35747
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests_integrationtest.py
@@ -0,0 +1,1091 @@
+#!/usr/bin/python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# Copyright (C) 2010 Gabor Rapcsanyi (rgabor@inf.u-szeged.hu), University of Szeged
+# Copyright (C) 2011 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:
+#
+#     * 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 codecs
+import itertools
+import json
+import logging
+import os
+import platform
+import Queue
+import re
+import StringIO
+import sys
+import thread
+import time
+import threading
+import unittest
+
+from webkitpy.common.system import outputcapture, path
+from webkitpy.common.system.crashlogs_unittest import make_mock_crash_report_darwin
+from webkitpy.common.system.systemhost import SystemHost
+from webkitpy.common.host import Host
+from webkitpy.common.host_mock import MockHost
+
+from webkitpy.layout_tests import port
+from webkitpy.layout_tests import run_webkit_tests
+from webkitpy.layout_tests.controllers.manager import WorkerException
+from webkitpy.layout_tests.port import Port
+from webkitpy.layout_tests.port.test import TestPort, TestDriver
+from webkitpy.test.skip import skip_if
+from webkitpy.tool.mocktool import MockOptions
+
+
+def parse_args(extra_args=None, record_results=False, tests_included=False, new_results=False, print_nothing=True):
+    extra_args = extra_args or []
+    args = []
+    if not '--platform' in extra_args:
+        args.extend(['--platform', 'test'])
+    if not record_results:
+        args.append('--no-record-results')
+    if not new_results:
+        args.append('--no-new-test-results')
+
+    if not '--child-processes' in extra_args:
+        args.extend(['--child-processes', 1])
+    args.extend(extra_args)
+    if not tests_included:
+        # We use the glob to test that globbing works.
+        args.extend(['passes',
+                     'http/tests',
+                     'websocket/tests',
+                     'failures/expected/*'])
+    return run_webkit_tests.parse_args(args)
+
+
+def passing_run(extra_args=None, port_obj=None, record_results=False, tests_included=False, host=None, shared_port=True):
+    options, parsed_args = parse_args(extra_args, record_results, tests_included)
+    if not port_obj:
+        host = host or MockHost()
+        port_obj = host.port_factory.get(port_name=options.platform, options=options)
+
+    if shared_port:
+        port_obj.host.port_factory.get = lambda *args, **kwargs: port_obj
+
+    buildbot_output = StringIO.StringIO()
+    regular_output = StringIO.StringIO()
+    res = run_webkit_tests.run(port_obj, options, parsed_args, buildbot_output=buildbot_output, regular_output=regular_output)
+    return res == 0
+
+
+def logging_run(extra_args=None, port_obj=None, record_results=False, tests_included=False, host=None, new_results=False, shared_port=True):
+    options, parsed_args = parse_args(extra_args=extra_args,
+                                      record_results=record_results,
+                                      tests_included=tests_included,
+                                      print_nothing=False, new_results=new_results)
+    host = host or MockHost()
+    if not port_obj:
+        port_obj = host.port_factory.get(port_name=options.platform, options=options)
+
+    res, buildbot_output, regular_output = run_and_capture(port_obj, options, parsed_args, shared_port)
+    return (res, buildbot_output, regular_output, host.user)
+
+
+def run_and_capture(port_obj, options, parsed_args, shared_port=True):
+    if shared_port:
+        port_obj.host.port_factory.get = lambda *args, **kwargs: port_obj
+    oc = outputcapture.OutputCapture()
+    try:
+        oc.capture_output()
+        buildbot_output = StringIO.StringIO()
+        regular_output = StringIO.StringIO()
+        res = run_webkit_tests.run(port_obj, options, parsed_args,
+                                   buildbot_output=buildbot_output,
+                                   regular_output=regular_output)
+    finally:
+        oc.restore_output()
+    return (res, buildbot_output, regular_output)
+
+
+def get_tests_run(extra_args=None, tests_included=False, flatten_batches=False,
+                  host=None, include_reference_html=False):
+    extra_args = extra_args or []
+    if not tests_included:
+        # Not including http tests since they get run out of order (that
+        # behavior has its own test, see test_get_test_file_queue)
+        extra_args = ['passes', 'failures'] + extra_args
+    options, parsed_args = parse_args(extra_args, tests_included=True)
+
+    host = host or MockHost()
+    test_batches = []
+
+    class RecordingTestDriver(TestDriver):
+        def __init__(self, port, worker_number):
+            TestDriver.__init__(self, port, worker_number, pixel_tests=port.get_option('pixel_test'), no_timeout=False)
+            self._current_test_batch = None
+
+        def start(self):
+            pass
+
+        def stop(self):
+            self._current_test_batch = None
+
+        def run_test(self, test_input, stop_when_done):
+            if self._current_test_batch is None:
+                self._current_test_batch = []
+                test_batches.append(self._current_test_batch)
+            test_name = test_input.test_name
+            # In case of reftest, one test calls the driver's run_test() twice.
+            # We should not add a reference html used by reftests to tests unless include_reference_html parameter
+            # is explicitly given.
+            filesystem = self._port.host.filesystem
+            dirname, filename = filesystem.split(test_name)
+            if include_reference_html or not Port.is_reference_html_file(filesystem, dirname, filename):
+                self._current_test_batch.append(test_name)
+            return TestDriver.run_test(self, test_input, stop_when_done)
+
+    class RecordingTestPort(TestPort):
+        def create_driver(self, worker_number):
+            return RecordingTestDriver(self, worker_number)
+
+    recording_port = RecordingTestPort(host, options=options)
+    run_and_capture(recording_port, options, parsed_args)
+
+    if flatten_batches:
+        return list(itertools.chain(*test_batches))
+
+    return test_batches
+
+
+# Update this magic number if you add an unexpected test to webkitpy.layout_tests.port.test
+# FIXME: It's nice to have a routine in port/test.py that returns this number.
+unexpected_failures = 12
+unexpected_tests_count = unexpected_failures + 4
+
+
+class StreamTestingMixin(object):
+    def assertContains(self, stream, string):
+        self.assertTrue(string in stream.getvalue())
+
+    def assertEmpty(self, stream):
+        self.assertFalse(stream.getvalue())
+
+    def assertNotEmpty(self, stream):
+        self.assertTrue(stream.getvalue())
+
+
+class LintTest(unittest.TestCase, StreamTestingMixin):
+    def test_all_configurations(self):
+
+        class FakePort(object):
+            def __init__(self, host, name, path):
+                self.host = host
+                self.name = name
+                self.path = path
+
+            def test_configuration(self):
+                return None
+
+            def expectations_dict(self):
+                self.host.ports_parsed.append(self.name)
+                return {self.path: ''}
+
+            def skipped_layout_tests(self, tests):
+                return set([])
+
+            def all_test_configurations(self):
+                return []
+
+            def configuration_specifier_macros(self):
+                return []
+
+            def path_from_webkit_base(self):
+                return ''
+
+            def get_option(self, name, val):
+                return val
+
+        class FakeFactory(object):
+            def __init__(self, host, ports):
+                self.host = host
+                self.ports = {}
+                for port in ports:
+                    self.ports[port.name] = port
+
+            def get(self, port_name, *args, **kwargs):
+                return self.ports[port_name]
+
+            def all_port_names(self):
+                return sorted(self.ports.keys())
+
+        host = MockHost()
+        host.ports_parsed = []
+        host.port_factory = FakeFactory(host, (FakePort(host, 'a', 'path-to-a'),
+                                               FakePort(host, 'b', 'path-to-b'),
+                                               FakePort(host, 'b-win', 'path-to-b')))
+
+        self.assertEquals(run_webkit_tests.lint(host.port_factory.ports['a'], MockOptions(platform=None)), 0)
+        self.assertEquals(host.ports_parsed, ['a', 'b', 'b-win'])
+
+        host.ports_parsed = []
+        self.assertEquals(run_webkit_tests.lint(host.port_factory.ports['a'], MockOptions(platform='a')), 0)
+        self.assertEquals(host.ports_parsed, ['a'])
+
+    def test_lint_test_files(self):
+        res, out, err, user = logging_run(['--lint-test-files'])
+        self.assertEqual(res, 0)
+        self.assertEmpty(out)
+        self.assertContains(err, 'Lint succeeded')
+
+    def test_lint_test_files__errors(self):
+        options, parsed_args = parse_args(['--lint-test-files'])
+        host = MockHost()
+        port_obj = host.port_factory.get(options.platform, options=options)
+        port_obj.expectations_dict = lambda: {'': '-- syntax error'}
+        res, out, err = run_and_capture(port_obj, options, parsed_args)
+
+        self.assertEqual(res, -1)
+        self.assertEmpty(out)
+        self.assertTrue(any(['Lint failed' in msg for msg in err.buflist]))
+
+        # ensure we lint *all* of the files in the cascade.
+        port_obj.expectations_dict = lambda: {'foo': '-- syntax error1', 'bar': '-- syntax error2'}
+        res, out, err = run_and_capture(port_obj, options, parsed_args)
+
+        self.assertEqual(res, -1)
+        self.assertEmpty(out)
+        self.assertTrue(any(['foo:1' in msg for msg in err.buflist]))
+        self.assertTrue(any(['bar:1' in msg for msg in err.buflist]))
+
+
+class MainTest(unittest.TestCase, StreamTestingMixin):
+    def setUp(self):
+        # A real PlatformInfo object is used here instead of a
+        # MockPlatformInfo because we need to actually check for
+        # Windows and Mac to skip some tests.
+        self._platform = SystemHost().platform
+
+        # FIXME: Remove this when we fix test-webkitpy to work
+        # properly on cygwin (bug 63846).
+        self.should_test_processes = not self._platform.is_win()
+
+    def test_accelerated_compositing(self):
+        # This just tests that we recognize the command line args
+        self.assertTrue(passing_run(['--accelerated-video']))
+        self.assertTrue(passing_run(['--no-accelerated-video']))
+
+    def test_accelerated_2d_canvas(self):
+        # This just tests that we recognize the command line args
+        self.assertTrue(passing_run(['--accelerated-2d-canvas']))
+        self.assertTrue(passing_run(['--no-accelerated-2d-canvas']))
+
+    def test_all(self):
+        res, out, err, user = logging_run([], tests_included=True)
+        self.assertEquals(res, unexpected_tests_count)
+
+    def test_basic(self):
+        self.assertTrue(passing_run())
+
+    def test_batch_size(self):
+        batch_tests_run = get_tests_run(['--batch-size', '2'])
+        for batch in batch_tests_run:
+            self.assertTrue(len(batch) <= 2, '%s had too many tests' % ', '.join(batch))
+
+    def test_max_locked_shards(self):
+        # Tests for the default of using one locked shard even in the case of more than one child process.
+        if not self.should_test_processes:
+            return
+        save_env_webkit_test_max_locked_shards = None
+        if "WEBKIT_TEST_MAX_LOCKED_SHARDS" in os.environ:
+            save_env_webkit_test_max_locked_shards = os.environ["WEBKIT_TEST_MAX_LOCKED_SHARDS"]
+            del os.environ["WEBKIT_TEST_MAX_LOCKED_SHARDS"]
+        _, _, regular_output, _ = logging_run(['--debug-rwt-logging', '--child-processes', '2'], shared_port=False)
+        try:
+            self.assertTrue(any(['(1 locked)' in line for line in regular_output.buflist]))
+        finally:
+            if save_env_webkit_test_max_locked_shards:
+                os.environ["WEBKIT_TEST_MAX_LOCKED_SHARDS"] = save_env_webkit_test_max_locked_shards
+
+    def test_child_processes_2(self):
+        if self.should_test_processes:
+            _, _, regular_output, _ = logging_run(
+                ['--debug-rwt-logging', '--child-processes', '2'], shared_port=False)
+            self.assertTrue(any(['Running 2 ' in line for line in regular_output.buflist]))
+
+    def test_child_processes_min(self):
+        if self.should_test_processes:
+            _, _, regular_output, _ = logging_run(
+                ['--debug-rwt-logging', '--child-processes', '2', '-i', 'passes/passes', 'passes'],
+                tests_included=True, shared_port=False)
+            self.assertTrue(any(['Running 1 ' in line for line in regular_output.buflist]))
+
+    def test_dryrun(self):
+        batch_tests_run = get_tests_run(['--dry-run'])
+        self.assertEqual(batch_tests_run, [])
+
+        batch_tests_run = get_tests_run(['-n'])
+        self.assertEqual(batch_tests_run, [])
+
+    def test_exception_raised(self):
+        # Exceptions raised by a worker are treated differently depending on
+        # whether they are in-process or out. inline exceptions work as normal,
+        # which allows us to get the full stack trace and traceback from the
+        # worker. The downside to this is that it could be any error, but this
+        # is actually useful in testing.
+        #
+        # Exceptions raised in a separate process are re-packaged into
+        # WorkerExceptions, which have a string capture of the stack which can
+        # be printed, but don't display properly in the unit test exception handlers.
+        self.assertRaises(ValueError, logging_run,
+            ['failures/expected/exception.html', '--child-processes', '1'], tests_included=True)
+
+        if self.should_test_processes:
+            self.assertRaises(WorkerException, logging_run,
+                ['--child-processes', '2', '--force', 'failures/expected/exception.html', 'passes/text.html'], tests_included=True, shared_port=False)
+
+    def test_full_results_html(self):
+        # FIXME: verify html?
+        res, out, err, user = logging_run(['--full-results-html'])
+        self.assertEqual(res, 0)
+
+    def test_hung_thread(self):
+        res, out, err, user = logging_run(['--run-singly', '--time-out-ms=50',
+                                          'failures/expected/hang.html'],
+                                          tests_included=True)
+        # Note that hang.html is marked as WontFix and all WontFix tests are
+        # expected to Pass, so that actually running them generates an "unexpected" error.
+        self.assertEqual(res, 1)
+        self.assertNotEmpty(out)
+        self.assertNotEmpty(err)
+
+    def test_keyboard_interrupt(self):
+        # Note that this also tests running a test marked as SKIP if
+        # you specify it explicitly.
+        self.assertRaises(KeyboardInterrupt, logging_run,
+            ['failures/expected/keyboard.html', '--child-processes', '1'],
+            tests_included=True)
+
+        if self.should_test_processes:
+            self.assertRaises(KeyboardInterrupt, logging_run,
+                ['failures/expected/keyboard.html', 'passes/text.html', '--child-processes', '2', '--force'], tests_included=True, shared_port=False)
+
+    def test_no_tests_found(self):
+        res, out, err, user = logging_run(['resources'], tests_included=True)
+        self.assertEqual(res, -1)
+        self.assertEmpty(out)
+        self.assertContains(err, 'No tests to run.\n')
+
+    def test_no_tests_found_2(self):
+        res, out, err, user = logging_run(['foo'], tests_included=True)
+        self.assertEqual(res, -1)
+        self.assertEmpty(out)
+        self.assertContains(err, 'No tests to run.\n')
+
+    def test_randomize_order(self):
+        # FIXME: verify order was shuffled
+        self.assertTrue(passing_run(['--randomize-order']))
+
+    def test_gc_between_tests(self):
+        self.assertTrue(passing_run(['--gc-between-tests']))
+
+    def test_complex_text(self):
+        self.assertTrue(passing_run(['--complex-text']))
+
+    def test_threaded(self):
+        self.assertTrue(passing_run(['--threaded']))
+
+    def test_repeat_each(self):
+        tests_to_run = ['passes/image.html', 'passes/text.html']
+        tests_run = get_tests_run(['--repeat-each', '2'] + tests_to_run, tests_included=True, flatten_batches=True)
+        self.assertEquals(tests_run, ['passes/image.html', 'passes/image.html', 'passes/text.html', 'passes/text.html'])
+
+    def test_ignore_flag(self):
+        # Note that passes/image.html is expected to be run since we specified it directly.
+        tests_run = get_tests_run(['-i', 'passes', 'passes/image.html'], flatten_batches=True, tests_included=True)
+        self.assertFalse('passes/text.html' in tests_run)
+        self.assertTrue('passes/image.html' in tests_run)
+
+    def test_skipped_flag(self):
+        tests_run = get_tests_run(['passes'], tests_included=True, flatten_batches=True)
+        self.assertFalse('passes/skipped/skip.html' in tests_run)
+        num_tests_run_by_default = len(tests_run)
+
+        # Check that nothing changes when we specify skipped=default.
+        self.assertEquals(len(get_tests_run(['--skipped=default', 'passes'], tests_included=True, flatten_batches=True)),
+                          num_tests_run_by_default)
+
+        # Now check that we run one more test (the skipped one).
+        tests_run = get_tests_run(['--skipped=ignore', 'passes'], tests_included=True, flatten_batches=True)
+        self.assertTrue('passes/skipped/skip.html' in tests_run)
+        self.assertEquals(len(tests_run), num_tests_run_by_default + 1)
+
+        # Now check that we only run the skipped test.
+        self.assertEquals(get_tests_run(['--skipped=only', 'passes'], tests_included=True, flatten_batches=True),
+                          ['passes/skipped/skip.html'])
+
+        # Now check that we don't run anything.
+        self.assertEquals(get_tests_run(['--skipped=always', 'passes/skipped/skip.html'], tests_included=True, flatten_batches=True),
+                          [])
+
+    def test_iterations(self):
+        tests_to_run = ['passes/image.html', 'passes/text.html']
+        tests_run = get_tests_run(['--iterations', '2'] + tests_to_run, tests_included=True, flatten_batches=True)
+        self.assertEquals(tests_run, ['passes/image.html', 'passes/text.html', 'passes/image.html', 'passes/text.html'])
+
+    def test_repeat_each_iterations_num_tests(self):
+        # The total number of tests should be: number_of_tests *
+        # repeat_each * iterations
+        host = MockHost()
+        res, out, err, _ = logging_run(['--iterations', '2',
+                                        '--repeat-each', '4',
+                                        '--debug-rwt-logging',
+                                        'passes/text.html', 'failures/expected/text.html'],
+                                       tests_included=True, host=host, record_results=True)
+        self.assertContains(out, "=> Results: 8/16 tests passed (50.0%)\n")
+        self.assertContains(err, "All 16 tests ran as expected.\n")
+
+    def test_run_chunk(self):
+        # Test that we actually select the right chunk
+        all_tests_run = get_tests_run(flatten_batches=True)
+        chunk_tests_run = get_tests_run(['--run-chunk', '1:4'], flatten_batches=True)
+        self.assertEquals(all_tests_run[4:8], chunk_tests_run)
+
+        # Test that we wrap around if the number of tests is not evenly divisible by the chunk size
+        tests_to_run = ['passes/error.html', 'passes/image.html', 'passes/platform_image.html', 'passes/text.html']
+        chunk_tests_run = get_tests_run(['--run-chunk', '1:3'] + tests_to_run, tests_included=True, flatten_batches=True)
+        self.assertEquals(['passes/text.html', 'passes/error.html', 'passes/image.html'], chunk_tests_run)
+
+    def test_run_force(self):
+        # This raises an exception because we run
+        # failures/expected/exception.html, which is normally SKIPped.
+
+        # See also the comments in test_exception_raised() about ValueError vs. WorkerException.
+        self.assertRaises(ValueError, logging_run, ['--force'])
+
+    def test_run_part(self):
+        # Test that we actually select the right part
+        tests_to_run = ['passes/error.html', 'passes/image.html', 'passes/platform_image.html', 'passes/text.html']
+        tests_run = get_tests_run(['--run-part', '1:2'] + tests_to_run, tests_included=True, flatten_batches=True)
+        self.assertEquals(['passes/error.html', 'passes/image.html'], tests_run)
+
+        # Test that we wrap around if the number of tests is not evenly divisible by the chunk size
+        # (here we end up with 3 parts, each with 2 tests, and we only have 4 tests total, so the
+        # last part repeats the first two tests).
+        chunk_tests_run = get_tests_run(['--run-part', '3:3'] + tests_to_run, tests_included=True, flatten_batches=True)
+        self.assertEquals(['passes/error.html', 'passes/image.html'], chunk_tests_run)
+
+    def test_run_singly(self):
+        batch_tests_run = get_tests_run(['--run-singly'])
+        for batch in batch_tests_run:
+            self.assertEquals(len(batch), 1, '%s had too many tests' % ', '.join(batch))
+
+    def test_skip_failing_tests(self):
+        # This tests that we skip both known failing and known flaky tests. Because there are
+        # no known flaky tests in the default test_expectations, we add additional expectations.
+        host = MockHost()
+        host.filesystem.write_text_file('/tmp/overrides.txt', 'Bug(x) passes/image.html [ ImageOnlyFailure Pass ]\n')
+
+        batches = get_tests_run(['--skip-failing-tests', '--additional-expectations', '/tmp/overrides.txt'], host=host)
+        has_passes_text = False
+        for batch in batches:
+            self.assertFalse('failures/expected/text.html' in batch)
+            self.assertFalse('passes/image.html' in batch)
+            has_passes_text = has_passes_text or ('passes/text.html' in batch)
+        self.assertTrue(has_passes_text)
+
+    def test_run_singly_actually_runs_tests(self):
+        res, _, _, _ = logging_run(['--run-singly', 'failures/unexpected'])
+        self.assertEquals(res, unexpected_failures)
+
+    def test_single_file(self):
+        # FIXME: We should consider replacing more of the get_tests_run()-style tests
+        # with tests that read the tests_run* files, like this one.
+        host = MockHost()
+        tests_run = passing_run(['passes/text.html'], tests_included=True, host=host)
+        self.assertEquals(host.filesystem.read_text_file('/tmp/layout-test-results/tests_run0.txt'),
+                          'passes/text.html\n')
+
+    def test_single_file_with_prefix(self):
+        tests_run = get_tests_run(['LayoutTests/passes/text.html'], tests_included=True, flatten_batches=True)
+        self.assertEquals(['passes/text.html'], tests_run)
+
+    def test_single_skipped_file(self):
+        tests_run = get_tests_run(['failures/expected/keybaord.html'], tests_included=True, flatten_batches=True)
+        self.assertEquals([], tests_run)
+
+    def test_stderr_is_saved(self):
+        host = MockHost()
+        self.assertTrue(passing_run(host=host))
+        self.assertEquals(host.filesystem.read_text_file('/tmp/layout-test-results/passes/error-stderr.txt'),
+                          'stuff going to stderr')
+
+    def test_test_list(self):
+        host = MockHost()
+        filename = '/tmp/foo.txt'
+        host.filesystem.write_text_file(filename, 'passes/text.html')
+        tests_run = get_tests_run(['--test-list=%s' % filename], tests_included=True, flatten_batches=True, host=host)
+        self.assertEquals(['passes/text.html'], tests_run)
+        host.filesystem.remove(filename)
+        res, out, err, user = logging_run(['--test-list=%s' % filename],
+                                          tests_included=True, host=host)
+        self.assertEqual(res, -1)
+        self.assertNotEmpty(err)
+
+    def test_test_list_with_prefix(self):
+        host = MockHost()
+        filename = '/tmp/foo.txt'
+        host.filesystem.write_text_file(filename, 'LayoutTests/passes/text.html')
+        tests_run = get_tests_run(['--test-list=%s' % filename], tests_included=True, flatten_batches=True, host=host)
+        self.assertEquals(['passes/text.html'], tests_run)
+
+    def test_unexpected_failures(self):
+        # Run tests including the unexpected failures.
+        self._url_opened = None
+        res, out, err, user = logging_run(tests_included=True)
+
+        self.assertEqual(res, unexpected_tests_count)
+        self.assertNotEmpty(out)
+        self.assertNotEmpty(err)
+        self.assertEqual(user.opened_urls, [path.abspath_to_uri(MockHost().platform, '/tmp/layout-test-results/results.html')])
+
+    def test_missing_and_unexpected_results(self):
+        # Test that we update expectations in place. If the expectation
+        # is missing, update the expected generic location.
+        host = MockHost()
+        res, out, err, _ = logging_run(['--no-show-results',
+            'failures/expected/missing_image.html',
+            'failures/unexpected/missing_text.html',
+            'failures/unexpected/text-image-checksum.html'],
+            tests_included=True, host=host, record_results=True)
+        file_list = host.filesystem.written_files.keys()
+        file_list.remove('/tmp/layout-test-results/tests_run0.txt')
+        self.assertEquals(res, 1)
+        expected_token = '"unexpected":{"text-image-checksum.html":{"expected":"PASS","actual":"IMAGE+TEXT","image_diff_percent":1},"missing_text.html":{"expected":"PASS","is_missing_text":true,"actual":"MISSING"}'
+        json_string = host.filesystem.read_text_file('/tmp/layout-test-results/full_results.json')
+        self.assertTrue(json_string.find(expected_token) != -1)
+        self.assertTrue(json_string.find('"num_regressions":1') != -1)
+        self.assertTrue(json_string.find('"num_flaky":0') != -1)
+        self.assertTrue(json_string.find('"num_missing":1') != -1)
+
+    def test_pixel_test_directories(self):
+        host = MockHost()
+
+        """Both tests have faling checksum. We include only the first in pixel tests so only that should fail."""
+        args = ['--pixel-tests', '--pixel-test-directory', 'failures/unexpected/pixeldir',
+                'failures/unexpected/pixeldir/image_in_pixeldir.html',
+                'failures/unexpected/image_not_in_pixeldir.html']
+        res, out, err, _ = logging_run(extra_args=args, host=host, record_results=True, tests_included=True)
+
+        self.assertEquals(res, 1)
+        expected_token = '"unexpected":{"pixeldir":{"image_in_pixeldir.html":{"expected":"PASS","actual":"IMAGE"'
+        json_string = host.filesystem.read_text_file('/tmp/layout-test-results/full_results.json')
+        self.assertTrue(json_string.find(expected_token) != -1)
+
+    def test_missing_and_unexpected_results_with_custom_exit_code(self):
+        # Test that we update expectations in place. If the expectation
+        # is missing, update the expected generic location.
+        class CustomExitCodePort(TestPort):
+            def exit_code_from_summarized_results(self, unexpected_results):
+                return unexpected_results['num_regressions'] + unexpected_results['num_missing']
+
+        host = MockHost()
+        options, parsed_args = run_webkit_tests.parse_args(['--pixel-tests', '--no-new-test-results'])
+        test_port = CustomExitCodePort(host, options=options)
+        res, out, err, _ = logging_run(['--no-show-results',
+            'failures/expected/missing_image.html',
+            'failures/unexpected/missing_text.html',
+            'failures/unexpected/text-image-checksum.html'],
+            tests_included=True, host=host, record_results=True, port_obj=test_port)
+        self.assertEquals(res, 2)
+
+    def test_crash_with_stderr(self):
+        host = MockHost()
+        res, buildbot_output, regular_output, user = logging_run([
+                'failures/unexpected/crash-with-stderr.html',
+            ],
+            tests_included=True,
+            record_results=True,
+            host=host)
+        self.assertTrue(host.filesystem.read_text_file('/tmp/layout-test-results/full_results.json').find('{"crash-with-stderr.html":{"expected":"PASS","actual":"CRASH","has_stderr":true}}') != -1)
+
+    def test_no_image_failure_with_image_diff(self):
+        host = MockHost()
+        res, buildbot_output, regular_output, user = logging_run([
+                'failures/unexpected/checksum-with-matching-image.html',
+            ],
+            tests_included=True,
+            record_results=True,
+            host=host)
+        self.assertTrue(host.filesystem.read_text_file('/tmp/layout-test-results/full_results.json').find('"num_regressions":0') != -1)
+
+    def test_crash_log(self):
+        # FIXME: Need to rewrite these tests to not be mac-specific, or move them elsewhere.
+        # Currently CrashLog uploading only works on Darwin.
+        if not self._platform.is_mac():
+            return
+        mock_crash_report = make_mock_crash_report_darwin('DumpRenderTree', 12345)
+        host = MockHost()
+        host.filesystem.write_text_file('/Users/mock/Library/Logs/DiagnosticReports/DumpRenderTree_2011-06-13-150719_quadzen.crash', mock_crash_report)
+        res, buildbot_output, regular_output, user = logging_run([
+                'failures/unexpected/crash-with-stderr.html',
+            ],
+            tests_included=True,
+            record_results=True,
+            host=host)
+        expected_crash_log = mock_crash_report
+        self.assertEquals(host.filesystem.read_text_file('/tmp/layout-test-results/failures/unexpected/crash-with-stderr-crash-log.txt'), expected_crash_log)
+
+    def test_web_process_crash_log(self):
+        # FIXME: Need to rewrite these tests to not be mac-specific, or move them elsewhere.
+        # Currently CrashLog uploading only works on Darwin.
+        if not self._platform.is_mac():
+            return
+        mock_crash_report = make_mock_crash_report_darwin('WebProcess', 12345)
+        host = MockHost()
+        host.filesystem.write_text_file('/Users/mock/Library/Logs/DiagnosticReports/WebProcess_2011-06-13-150719_quadzen.crash', mock_crash_report)
+        res, buildbot_output, regular_output, user = logging_run([
+                'failures/unexpected/web-process-crash-with-stderr.html',
+            ],
+            tests_included=True,
+            record_results=True,
+            host=host)
+        self.assertEquals(host.filesystem.read_text_file('/tmp/layout-test-results/failures/unexpected/web-process-crash-with-stderr-crash-log.txt'), mock_crash_report)
+
+    def test_exit_after_n_failures_upload(self):
+        host = MockHost()
+        res, buildbot_output, regular_output, user = logging_run([
+                'failures/unexpected/text-image-checksum.html',
+                'passes/text.html',
+                '--exit-after-n-failures', '1',
+            ],
+            tests_included=True,
+            record_results=True,
+            host=host)
+
+        # By returning False, we know that the incremental results were generated and then deleted.
+        self.assertFalse(host.filesystem.exists('/tmp/layout-test-results/incremental_results.json'))
+
+        # This checks that we report only the number of tests that actually failed.
+        self.assertEquals(res, 1)
+
+        # This checks that passes/text.html is considered SKIPped.
+        self.assertTrue('"skipped":1' in host.filesystem.read_text_file('/tmp/layout-test-results/full_results.json'))
+
+        # This checks that we told the user we bailed out.
+        self.assertTrue('Exiting early after 1 failures. 1 tests run.\n' in regular_output.getvalue())
+
+        # This checks that neither test ran as expected.
+        # FIXME: This log message is confusing; tests that were skipped should be called out separately.
+        self.assertTrue('0 tests ran as expected, 2 didn\'t:\n' in regular_output.getvalue())
+
+    def test_exit_after_n_failures(self):
+        # Unexpected failures should result in tests stopping.
+        tests_run = get_tests_run([
+                'failures/unexpected/text-image-checksum.html',
+                'passes/text.html',
+                '--exit-after-n-failures', '1',
+            ],
+            tests_included=True,
+            flatten_batches=True)
+        self.assertEquals(['failures/unexpected/text-image-checksum.html'], tests_run)
+
+        # But we'll keep going for expected ones.
+        tests_run = get_tests_run([
+                'failures/expected/text.html',
+                'passes/text.html',
+                '--exit-after-n-failures', '1',
+            ],
+            tests_included=True,
+            flatten_batches=True)
+        self.assertEquals(['failures/expected/text.html', 'passes/text.html'], tests_run)
+
+    def test_exit_after_n_crashes(self):
+        # Unexpected crashes should result in tests stopping.
+        tests_run = get_tests_run([
+                'failures/unexpected/crash.html',
+                'passes/text.html',
+                '--exit-after-n-crashes-or-timeouts', '1',
+            ],
+            tests_included=True,
+            flatten_batches=True)
+        self.assertEquals(['failures/unexpected/crash.html'], tests_run)
+
+        # Same with timeouts.
+        tests_run = get_tests_run([
+                'failures/unexpected/timeout.html',
+                'passes/text.html',
+                '--exit-after-n-crashes-or-timeouts', '1',
+            ],
+            tests_included=True,
+            flatten_batches=True)
+        self.assertEquals(['failures/unexpected/timeout.html'], tests_run)
+
+        # But we'll keep going for expected ones.
+        tests_run = get_tests_run([
+                'failures/expected/crash.html',
+                'passes/text.html',
+                '--exit-after-n-crashes-or-timeouts', '1',
+            ],
+            tests_included=True,
+            flatten_batches=True)
+        self.assertEquals(['failures/expected/crash.html', 'passes/text.html'], tests_run)
+
+    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.
+
+        host = MockHost()
+        with host.filesystem.mkdtemp() as tmpdir:
+            res, out, err, user = logging_run(['--results-directory=' + str(tmpdir)],
+                                              tests_included=True, host=host)
+            self.assertEqual(user.opened_urls, [path.abspath_to_uri(host.platform, host.filesystem.join(tmpdir, 'results.html'))])
+
+    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.opened_urls, [path.abspath_to_uri(MockHost().platform, '/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.
+        host = MockHost()
+        host.filesystem.maybe_make_directory('/tmp/cwd')
+        host.filesystem.chdir('/tmp/cwd')
+        res, out, err, user = logging_run(['--results-directory=foo'],
+                                          tests_included=True, host=host)
+        self.assertEqual(user.opened_urls, [path.abspath_to_uri(host.platform, '/tmp/cwd/foo/results.html')])
+
+    def test_retrying_and_flaky_tests(self):
+        host = MockHost()
+        res, out, err, _ = logging_run(['--debug-rwt-logging', 'failures/flaky'], tests_included=True, host=host)
+        self.assertEquals(res, 0)
+        self.assertTrue('Retrying' in err.getvalue())
+        self.assertTrue('Unexpected flakiness' in out.getvalue())
+        self.assertTrue(host.filesystem.exists('/tmp/layout-test-results/failures/flaky/text-actual.txt'))
+        self.assertTrue(host.filesystem.exists('/tmp/layout-test-results/retries/tests_run0.txt'))
+        self.assertFalse(host.filesystem.exists('/tmp/layout-test-results/retries/failures/flaky/text-actual.txt'))
+
+        # Now we test that --clobber-old-results does remove the old entries and the old retries,
+        # and that we don't retry again.
+        host = MockHost()
+        res, out, err, _ = logging_run(['--no-retry-failures', '--clobber-old-results', 'failures/flaky'], tests_included=True, host=host)
+        self.assertEquals(res, 1)
+        self.assertTrue('Clobbering old results' in err.getvalue())
+        self.assertTrue('flaky/text.html' in err.getvalue())
+        self.assertTrue('Unexpected text-only failures' in out.getvalue())
+        self.assertFalse('Unexpected flakiness' in out.getvalue())
+        self.assertTrue(host.filesystem.exists('/tmp/layout-test-results/failures/flaky/text-actual.txt'))
+        self.assertFalse(host.filesystem.exists('retries'))
+
+    def test_run_order__inline(self):
+        # These next tests test that we run the tests in ascending alphabetical
+        # order per directory. HTTP tests are sharded separately from other tests,
+        # so we have to test both.
+        tests_run = get_tests_run(['-i', 'passes/passes', 'passes'], tests_included=True, flatten_batches=True)
+        self.assertEquals(tests_run, sorted(tests_run))
+
+        tests_run = get_tests_run(['http/tests/passes'], tests_included=True, flatten_batches=True)
+        self.assertEquals(tests_run, sorted(tests_run))
+
+    def test_tolerance(self):
+        class ImageDiffTestPort(TestPort):
+            def diff_image(self, expected_contents, actual_contents, tolerance=None):
+                self.tolerance_used_for_diff_image = self._options.tolerance
+                return (True, 1, None)
+
+        def get_port_for_run(args):
+            options, parsed_args = run_webkit_tests.parse_args(args)
+            host = MockHost()
+            test_port = ImageDiffTestPort(host, options=options)
+            res = passing_run(args, port_obj=test_port, tests_included=True)
+            self.assertTrue(res)
+            return test_port
+
+        base_args = ['--pixel-tests', '--no-new-test-results', 'failures/expected/*']
+
+        # If we pass in an explicit tolerance argument, then that will be used.
+        test_port = get_port_for_run(base_args + ['--tolerance', '.1'])
+        self.assertEqual(0.1, test_port.tolerance_used_for_diff_image)
+        test_port = get_port_for_run(base_args + ['--tolerance', '0'])
+        self.assertEqual(0, test_port.tolerance_used_for_diff_image)
+
+        # Otherwise the port's default tolerance behavior (including ignoring it)
+        # should be used.
+        test_port = get_port_for_run(base_args)
+        self.assertEqual(None, test_port.tolerance_used_for_diff_image)
+
+    def test_virtual(self):
+        self.assertTrue(passing_run(['passes/text.html', 'passes/args.html',
+                                     'virtual/passes/text.html', 'virtual/passes/args.html']))
+
+    def test_reftest_run(self):
+        tests_run = get_tests_run(['passes/reftest.html'], tests_included=True, flatten_batches=True)
+        self.assertEquals(['passes/reftest.html'], tests_run)
+
+    def test_reftest_run_reftests_if_pixel_tests_are_disabled(self):
+        tests_run = get_tests_run(['--no-pixel-tests', 'passes/reftest.html'], tests_included=True, flatten_batches=True)
+        self.assertEquals(['passes/reftest.html'], tests_run)
+
+    def test_reftest_skip_reftests_if_no_ref_tests(self):
+        tests_run = get_tests_run(['--no-ref-tests', 'passes/reftest.html'], tests_included=True, flatten_batches=True)
+        self.assertEquals([], tests_run)
+        tests_run = get_tests_run(['--no-ref-tests', '--no-pixel-tests', 'passes/reftest.html'], tests_included=True, flatten_batches=True)
+        self.assertEquals([], tests_run)
+
+    def test_reftest_expected_html_should_be_ignored(self):
+        tests_run = get_tests_run(['passes/reftest-expected.html'], tests_included=True, flatten_batches=True)
+        self.assertEquals([], tests_run)
+
+    def test_reftest_driver_should_run_expected_html(self):
+        tests_run = get_tests_run(['passes/reftest.html'], tests_included=True, flatten_batches=True, include_reference_html=True)
+        self.assertEquals(['passes/reftest.html', 'passes/reftest-expected.html'], tests_run)
+
+    def test_reftest_driver_should_run_expected_mismatch_html(self):
+        tests_run = get_tests_run(['passes/mismatch.html'], tests_included=True, flatten_batches=True, include_reference_html=True)
+        self.assertEquals(['passes/mismatch.html', 'passes/mismatch-expected-mismatch.html'], tests_run)
+
+    def test_reftest_should_not_use_naming_convention_if_not_listed_in_reftestlist(self):
+        host = MockHost()
+        res, out, err, _ = logging_run(['--no-show-results', 'reftests/foo/'], tests_included=True, host=host, record_results=True)
+        json_string = host.filesystem.read_text_file('/tmp/layout-test-results/full_results.json')
+        self.assertTrue(json_string.find('"unlistedtest.html":{"expected":"PASS","is_missing_text":true,"actual":"MISSING","is_missing_image":true}') != -1)
+        self.assertTrue(json_string.find('"num_regressions":4') != -1)
+        self.assertTrue(json_string.find('"num_flaky":0') != -1)
+        self.assertTrue(json_string.find('"num_missing":1') != -1)
+
+    def test_additional_platform_directory(self):
+        self.assertTrue(passing_run(['--additional-platform-directory', '/tmp/foo']))
+        self.assertTrue(passing_run(['--additional-platform-directory', '/tmp/../foo']))
+        self.assertTrue(passing_run(['--additional-platform-directory', '/tmp/foo', '--additional-platform-directory', '/tmp/bar']))
+        self.assertTrue(passing_run(['--additional-platform-directory', 'foo']))
+
+    def test_additional_expectations(self):
+        host = MockHost()
+        host.filesystem.write_text_file('/tmp/overrides.txt', 'Bug(x) failures/unexpected/mismatch.html [ ImageOnlyFailure ]\n')
+        self.assertTrue(passing_run(['--additional-expectations', '/tmp/overrides.txt', 'failures/unexpected/mismatch.html'],
+                                     tests_included=True, host=host))
+
+    def test_no_http_and_force(self):
+        # See test_run_force, using --force raises an exception.
+        # FIXME: We would like to check the warnings generated.
+        self.assertRaises(ValueError, logging_run, ['--force', '--no-http'])
+
+    @staticmethod
+    def has_test_of_type(tests, type):
+        return [test for test in tests if type in test]
+
+    def test_no_http_tests(self):
+        batch_tests_dryrun = get_tests_run(['LayoutTests/http', 'websocket/'], flatten_batches=True)
+        self.assertTrue(MainTest.has_test_of_type(batch_tests_dryrun, 'http'))
+        self.assertTrue(MainTest.has_test_of_type(batch_tests_dryrun, 'websocket'))
+
+        batch_tests_run_no_http = get_tests_run(['--no-http', 'LayoutTests/http', 'websocket/'], flatten_batches=True)
+        self.assertFalse(MainTest.has_test_of_type(batch_tests_run_no_http, 'http'))
+        self.assertFalse(MainTest.has_test_of_type(batch_tests_run_no_http, 'websocket'))
+
+        batch_tests_run_http = get_tests_run(['--http', 'LayoutTests/http', 'websocket/'], flatten_batches=True)
+        self.assertTrue(MainTest.has_test_of_type(batch_tests_run_http, 'http'))
+        self.assertTrue(MainTest.has_test_of_type(batch_tests_run_http, 'websocket'))
+
+    def test_platform_tests_are_found(self):
+        tests_run = get_tests_run(['--platform', 'test-mac-leopard', 'http'], tests_included=True, flatten_batches=True)
+        self.assertTrue('platform/test-mac-leopard/http/test.html' in tests_run)
+        self.assertFalse('platform/test-win-win7/http/test.html' in tests_run)
+
+    def test_output_diffs(self):
+        # Test to ensure that we don't generate -wdiff.html or -pretty.html if wdiff and PrettyPatch
+        # aren't available.
+        host = MockHost()
+        res, out, err, _ = logging_run(['--pixel-tests', 'failures/unexpected/text-image-checksum.html'],
+                                       tests_included=True, record_results=True, host=host)
+        written_files = host.filesystem.written_files
+        self.assertTrue(any(path.endswith('-diff.txt') for path in written_files.keys()))
+        self.assertFalse(any(path.endswith('-wdiff.html') for path in written_files.keys()))
+        self.assertFalse(any(path.endswith('-pretty-diff.html') for path in written_files.keys()))
+
+        full_results_text = host.filesystem.read_text_file('/tmp/layout-test-results/full_results.json')
+        full_results = json.loads(full_results_text.replace("ADD_RESULTS(", "").replace(");", ""))
+        self.assertEquals(full_results['has_wdiff'], False)
+        self.assertEquals(full_results['has_pretty_patch'], False)
+
+    def test_unsupported_platform(self):
+        oc = outputcapture.OutputCapture()
+        try:
+            oc.capture_output()
+            res = run_webkit_tests.main(['--platform', 'foo'])
+        finally:
+            stdout, stderr, logs = oc.restore_output()
+
+        self.assertEquals(res, run_webkit_tests.EXCEPTIONAL_EXIT_STATUS)
+        self.assertEquals(stdout, '')
+        self.assertTrue('unsupported platform' in stderr)
+
+        # This is empty because we don't even get a chance to configure the logger before failing.
+        self.assertEquals(logs, '')
+
+    def test_verbose_in_child_processes(self):
+        # When we actually run multiple processes, we may have to reconfigure logging in the
+        # child process (e.g., on win32) and we need to make sure that works and we still
+        # see the verbose log output. However, we can't use logging_run() because using
+        # outputcapture to capture stdout and stderr latter results in a nonpicklable host.
+
+        # Test is flaky on Windows: https://bugs.webkit.org/show_bug.cgi?id=98559
+        if not self.should_test_processes:
+            return
+
+        options, parsed_args = parse_args(['--verbose', '--fully-parallel', '--child-processes', '2', 'passes/text.html', 'passes/image.html'], tests_included=True, print_nothing=False)
+        host = MockHost()
+        port_obj = host.port_factory.get(port_name=options.platform, options=options)
+        buildbot_output = StringIO.StringIO()
+        regular_output = StringIO.StringIO()
+        res = run_webkit_tests.run(port_obj, options, parsed_args, buildbot_output=buildbot_output, regular_output=regular_output)
+        self.assertTrue('text.html passed' in regular_output.getvalue())
+        self.assertTrue('image.html passed' in regular_output.getvalue())
+
+
+class EndToEndTest(unittest.TestCase):
+    def parse_full_results(self, full_results_text):
+        json_to_eval = full_results_text.replace("ADD_RESULTS(", "").replace(");", "")
+        compressed_results = json.loads(json_to_eval)
+        return compressed_results
+
+    def test_end_to_end(self):
+        host = MockHost()
+        res, out, err, user = logging_run(record_results=True, tests_included=True, host=host)
+
+        self.assertEquals(res, unexpected_tests_count)
+        results = self.parse_full_results(host.filesystem.read_text_file('/tmp/layout-test-results/full_results.json'))
+
+        # Check to ensure we're passing back image diff %age correctly.
+        self.assertEquals(results['tests']['failures']['expected']['image.html']['image_diff_percent'], 1)
+
+        # Check that we attempted to display the results page in a browser.
+        self.assertTrue(user.opened_urls)
+
+    def test_reftest_with_two_notrefs(self):
+        # Test that we update expectations in place. If the expectation
+        # is missing, update the expected generic location.
+        host = MockHost()
+        res, out, err, _ = logging_run(['--no-show-results', 'reftests/foo/'], tests_included=True, host=host, record_results=True)
+        file_list = host.filesystem.written_files.keys()
+        file_list.remove('/tmp/layout-test-results/tests_run0.txt')
+        json_string = host.filesystem.read_text_file('/tmp/layout-test-results/full_results.json')
+        json = self.parse_full_results(json_string)
+        self.assertTrue("multiple-match-success.html" not in json["tests"]["reftests"]["foo"])
+        self.assertTrue("multiple-mismatch-success.html" not in json["tests"]["reftests"]["foo"])
+        self.assertTrue("multiple-both-success.html" not in json["tests"]["reftests"]["foo"])
+        self.assertEqual(json["tests"]["reftests"]["foo"]["multiple-match-failure.html"],
+            {"expected": "PASS", "actual": "IMAGE", "reftest_type": ["=="], "image_diff_percent": 1})
+        self.assertEqual(json["tests"]["reftests"]["foo"]["multiple-mismatch-failure.html"],
+            {"expected": "PASS", "actual": "IMAGE", "reftest_type": ["!="]})
+        self.assertEqual(json["tests"]["reftests"]["foo"]["multiple-both-failure.html"],
+            {"expected": "PASS", "actual": "IMAGE", "reftest_type": ["==", "!="]})
+
+
+class RebaselineTest(unittest.TestCase, StreamTestingMixin):
+    def assertBaselines(self, file_list, file, extensions, err):
+        "assert that the file_list contains the baselines."""
+        for ext in extensions:
+            baseline = file + "-expected" + ext
+            baseline_msg = 'Writing new expected result "%s"\n' % baseline
+            self.assertTrue(any(f.find(baseline) != -1 for f in file_list))
+            self.assertContains(err, baseline_msg)
+
+    # FIXME: Add tests to ensure that we're *not* writing baselines when we're not
+    # supposed to be.
+
+    def test_reset_results(self):
+        # Test that we update expectations in place. If the expectation
+        # is missing, update the expected generic location.
+        host = MockHost()
+        res, out, err, _ = logging_run(['--pixel-tests',
+                        '--reset-results',
+                        'passes/image.html',
+                        'failures/expected/missing_image.html'],
+                        tests_included=True, host=host, new_results=True)
+        file_list = host.filesystem.written_files.keys()
+        file_list.remove('/tmp/layout-test-results/tests_run0.txt')
+        self.assertEquals(res, 0)
+        self.assertEmpty(out)
+        self.assertEqual(len(file_list), 4)
+        self.assertBaselines(file_list, "passes/image", [".txt", ".png"], err)
+        self.assertBaselines(file_list, "failures/expected/missing_image", [".txt", ".png"], err)
+
+    def test_missing_results(self):
+        # Test that we update expectations in place. If the expectation
+        # is missing, update the expected generic location.
+        host = MockHost()
+        res, out, err, _ = logging_run(['--no-show-results',
+                     'failures/unexpected/missing_text.html',
+                     'failures/unexpected/missing_image.html',
+                     'failures/unexpected/missing_audio.html',
+                     'failures/unexpected/missing_render_tree_dump.html'],
+                     tests_included=True, host=host, new_results=True)
+        file_list = host.filesystem.written_files.keys()
+        file_list.remove('/tmp/layout-test-results/tests_run0.txt')
+        self.assertEquals(res, 0)
+        self.assertNotEmpty(out)
+        self.assertEqual(len(file_list), 6)
+        self.assertBaselines(file_list, "failures/unexpected/missing_text", [".txt"], err)
+        self.assertBaselines(file_list, "platform/test/failures/unexpected/missing_image", [".png"], err)
+        self.assertBaselines(file_list, "platform/test/failures/unexpected/missing_render_tree_dump", [".txt"], err)
+
+    def test_new_baseline(self):
+        # Test that we update the platform expectations in the version-specific directories
+        # for both existing and new baselines.
+        host = MockHost()
+        res, out, err, _ = logging_run(['--pixel-tests',
+                        '--new-baseline',
+                        'passes/image.html',
+                        'failures/expected/missing_image.html'],
+                    tests_included=True, host=host, new_results=True)
+        file_list = host.filesystem.written_files.keys()
+        file_list.remove('/tmp/layout-test-results/tests_run0.txt')
+        self.assertEquals(res, 0)
+        self.assertEmpty(out)
+        self.assertEqual(len(file_list), 4)
+        self.assertBaselines(file_list,
+            "platform/test-mac-leopard/passes/image", [".txt", ".png"], err)
+        self.assertBaselines(file_list,
+            "platform/test-mac-leopard/failures/expected/missing_image", [".txt", ".png"], err)
+
+
+class PortTest(unittest.TestCase):
+    def assert_mock_port_works(self, port_name, args=[]):
+        self.assertTrue(passing_run(args + ['--platform', 'mock-' + port_name, 'fast/harness/results.html'], tests_included=True, host=Host()))
+
+    def disabled_test_chromium_mac_lion(self):
+        self.assert_mock_port_works('chromium-mac-lion')
+
+    def disabled_test_chromium_mac_lion_in_test_shell_mode(self):
+        self.assert_mock_port_works('chromium-mac-lion', args=['--additional-drt-flag=--test-shell'])
+
+    def disabled_test_qt_linux(self):
+        self.assert_mock_port_works('qt-linux')
+
+    def disabled_test_mac_lion(self):
+        self.assert_mock_port_works('mac-lion')
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/layout_tests/servers/__init__.py b/Tools/Scripts/webkitpy/layout_tests/servers/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/servers/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/layout_tests/servers/apache_http_server.py b/Tools/Scripts/webkitpy/layout_tests/servers/apache_http_server.py
new file mode 100644
index 0000000..7dede92
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/servers/apache_http_server.py
@@ -0,0 +1,193 @@
+#!/usr/bin/env python
+# Copyright (C) 2011 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 class to start/stop the apache http server used by layout tests."""
+
+
+import logging
+import os
+import re
+import socket
+import sys
+
+from webkitpy.layout_tests.servers import http_server_base
+
+
+_log = logging.getLogger(__name__)
+
+
+class LayoutTestApacheHttpd(http_server_base.HttpServerBase):
+    def __init__(self, port_obj, output_dir, additional_dirs=None, number_of_servers=None):
+        """Args:
+          port_obj: handle to the platform-specific routines
+          output_dir: the absolute path to the layout test result directory
+        """
+        http_server_base.HttpServerBase.__init__(self, port_obj, number_of_servers)
+        # We use the name "httpd" instead of "apache" to make our paths (e.g. the pid file: /tmp/WebKit/httpd.pid)
+        # match old-run-webkit-tests: https://bugs.webkit.org/show_bug.cgi?id=63956
+        self._name = 'httpd'
+        self._mappings = [{'port': 8000},
+                          {'port': 8080},
+                          {'port': 8443, 'sslcert': True}]
+        self._output_dir = output_dir
+        self._filesystem.maybe_make_directory(output_dir)
+
+        self._pid_file = self._filesystem.join(self._runtime_path, '%s.pid' % self._name)
+
+        test_dir = self._port_obj.layout_tests_dir()
+        js_test_resources_dir = self._filesystem.join(test_dir, "fast", "js", "resources")
+        media_resources_dir = self._filesystem.join(test_dir, "media")
+        mime_types_path = self._filesystem.join(test_dir, "http", "conf", "mime.types")
+        cert_file = self._filesystem.join(test_dir, "http", "conf", "webkit-httpd.pem")
+        access_log = self._filesystem.join(output_dir, "access_log.txt")
+        error_log = self._filesystem.join(output_dir, "error_log.txt")
+        document_root = self._filesystem.join(test_dir, "http", "tests")
+
+        # FIXME: We shouldn't be calling a protected method of _port_obj!
+        executable = self._port_obj._path_to_apache()
+
+        start_cmd = [executable,
+            '-f', "\"%s\"" % self._get_apache_config_file_path(test_dir, output_dir),
+            '-C', "\'DocumentRoot \"%s\"\'" % document_root,
+            '-c', "\'Alias /js-test-resources \"%s\"'" % js_test_resources_dir,
+            '-c', "\'Alias /media-resources \"%s\"'" % media_resources_dir,
+            '-c', "\'TypesConfig \"%s\"\'" % mime_types_path,
+            '-c', "\'CustomLog \"%s\" common\'" % access_log,
+            '-c', "\'ErrorLog \"%s\"\'" % error_log,
+            '-C', "\'User \"%s\"\'" % os.environ.get("USERNAME", os.environ.get("USER", "")),
+            '-c', "\'PidFile %s'" % self._pid_file,
+            '-k', "start"]
+
+        enable_ipv6 = self._port_obj.http_server_supports_ipv6()
+        # Perform part of the checks Apache's APR does when trying to listen to
+        # a specific host/port. This allows us to avoid trying to listen to
+        # IPV6 addresses when it fails on Apache. APR itself tries to call
+        # getaddrinfo() again without AI_ADDRCONFIG if the first call fails
+        # with EBADFLAGS, but that is not how it normally fails in our use
+        # cases, so ignore that for now.
+        # See https://bugs.webkit.org/show_bug.cgi?id=98602#c7
+        try:
+            socket.getaddrinfo('::1', 0, 0, 0, 0, socket.AI_ADDRCONFIG)
+        except:
+            enable_ipv6 = False
+
+        for mapping in self._mappings:
+            port = mapping['port']
+
+            start_cmd += ['-C', "\'Listen 127.0.0.1:%d\'" % port]
+
+            # We listen to both IPv4 and IPv6 loop-back addresses, but ignore
+            # requests to 8000 from random users on network.
+            # See https://bugs.webkit.org/show_bug.cgi?id=37104
+            if enable_ipv6:
+                start_cmd += ['-C', "\'Listen [::1]:%d\'" % port]
+
+        if additional_dirs:
+            for alias, path in additional_dirs.iteritems():
+                start_cmd += ['-c', "\'Alias %s \"%s\"\'" % (alias, path),
+                        # Disable CGI handler for additional dirs.
+                        '-c', "\'<Location %s>\'" % alias,
+                        '-c', "\'RemoveHandler .cgi .pl\'",
+                        '-c', "\'</Location>\'"]
+
+        if self._number_of_servers:
+            start_cmd += ['-c', "\'StartServers %d\'" % self._number_of_servers,
+                          '-c', "\'MinSpareServers %d\'" % self._number_of_servers,
+                          '-c', "\'MaxSpareServers %d\'" % self._number_of_servers]
+
+        stop_cmd = [executable,
+            '-f', "\"%s\"" % self._get_apache_config_file_path(test_dir, output_dir),
+            '-c', "\'PidFile %s'" % self._pid_file,
+            '-k', "stop"]
+
+        start_cmd.extend(['-c', "\'SSLCertificateFile %s\'" % cert_file])
+        # Join the string here so that Cygwin/Windows and Mac/Linux
+        # can use the same code. Otherwise, we could remove the single
+        # quotes above and keep cmd as a sequence.
+        # FIXME: It's unclear if this is still needed.
+        self._start_cmd = " ".join(start_cmd)
+        self._stop_cmd = " ".join(stop_cmd)
+
+    def _get_apache_config_file_path(self, test_dir, output_dir):
+        """Returns the path to the apache config file to use.
+        Args:
+          test_dir: absolute path to the LayoutTests directory.
+          output_dir: absolute path to the layout test results directory.
+        """
+        httpd_config = self._port_obj._path_to_apache_config_file()
+        httpd_config_copy = os.path.join(output_dir, "httpd.conf")
+        httpd_conf = self._filesystem.read_text_file(httpd_config)
+
+        # FIXME: Why do we need to copy the config file since we're not modifying it?
+        self._filesystem.write_text_file(httpd_config_copy, httpd_conf)
+
+        return httpd_config_copy
+
+    def _spawn_process(self):
+        _log.debug('Starting %s server, cmd="%s"' % (self._name, str(self._start_cmd)))
+        retval, err = self._run(self._start_cmd)
+        if retval or len(err):
+            raise http_server_base.ServerError('Failed to start %s: %s' % (self._name, err))
+
+        # For some reason apache isn't guaranteed to have created the pid file before
+        # the process exits, so we wait a little while longer.
+        if not self._wait_for_action(lambda: self._filesystem.exists(self._pid_file)):
+            raise http_server_base.ServerError('Failed to start %s: no pid file found' % self._name)
+
+        return int(self._filesystem.read_text_file(self._pid_file))
+
+    def _stop_running_server(self):
+        # If apache was forcefully killed, the pid file will not have been deleted, so check
+        # that the process specified by the pid_file no longer exists before deleting the file.
+        if self._pid and not self._executive.check_running_pid(self._pid):
+            self._filesystem.remove(self._pid_file)
+            return
+
+        retval, err = self._run(self._stop_cmd)
+        if retval or len(err):
+            raise http_server_base.ServerError('Failed to stop %s: %s' % (self._name, err))
+
+        # For some reason apache isn't guaranteed to have actually stopped after
+        # the stop command returns, so we wait a little while longer for the
+        # pid file to be removed.
+        if not self._wait_for_action(lambda: not self._filesystem.exists(self._pid_file)):
+            raise http_server_base.ServerError('Failed to stop %s: pid file still exists' % self._name)
+
+    def _run(self, cmd):
+        # Use shell=True because we join the arguments into a string for
+        # the sake of Window/Cygwin and it needs quoting that breaks
+        # shell=False.
+        # FIXME: We should not need to be joining shell arguments into strings.
+        # shell=True is a trail of tears.
+        # Note: Not thread safe: http://bugs.python.org/issue2320
+        process = self._executive.popen(cmd, shell=True, stderr=self._executive.PIPE)
+        process.wait()
+        retval = process.returncode
+        err = process.stderr.read()
+        return (retval, err)
diff --git a/Tools/Scripts/webkitpy/layout_tests/servers/apache_http_server_unittest.py b/Tools/Scripts/webkitpy/layout_tests/servers/apache_http_server_unittest.py
new file mode 100644
index 0000000..34ab97b
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/servers/apache_http_server_unittest.py
@@ -0,0 +1,70 @@
+# Copyright (C) 2012 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 re
+import sys
+import unittest
+
+from webkitpy.common.system.executive_mock import MockExecutive
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.host_mock import MockHost
+from webkitpy.layout_tests.port import test
+from webkitpy.layout_tests.servers.apache_http_server import LayoutTestApacheHttpd
+from webkitpy.layout_tests.servers.http_server_base import ServerError
+
+
+class TestLayoutTestApacheHttpd(unittest.TestCase):
+    def test_start_cmd(self):
+        # Fails on win - see https://bugs.webkit.org/show_bug.cgi?id=84726
+        if sys.platform in ('cygwin', 'win32'):
+            return
+
+        def fake_pid(_):
+            host.filesystem.write_text_file('/tmp/WebKit/httpd.pid', '42')
+            return True
+
+        host = MockHost()
+        host.executive = MockExecutive(should_log=True)
+        test_port = test.TestPort(host)
+        host.filesystem.write_text_file(test_port._path_to_apache_config_file(), '')
+
+        server = LayoutTestApacheHttpd(test_port, "/mock/output_dir", number_of_servers=4)
+        server._check_that_all_ports_are_available = lambda: True
+        server._is_server_running_on_all_ports = lambda: True
+        server._wait_for_action = fake_pid
+        oc = OutputCapture()
+        try:
+            oc.capture_output()
+            server.start()
+            server.stop()
+        finally:
+            out, err, logs = oc.restore_output()
+        self.assertTrue("StartServers 4" in err)
+        self.assertTrue("MinSpareServers 4" in err)
+        self.assertTrue("MaxSpareServers 4" in err)
+        self.assertTrue(host.filesystem.exists("/mock/output_dir/httpd.conf"))
diff --git a/Tools/Scripts/webkitpy/layout_tests/servers/http_server.py b/Tools/Scripts/webkitpy/layout_tests/servers/http_server.py
new file mode 100755
index 0000000..107c242
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/servers/http_server.py
@@ -0,0 +1,220 @@
+#!/usr/bin/env python
+# Copyright (C) 2011 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 class to help start/stop the lighttpd server used by layout tests."""
+
+import logging
+import os
+import time
+
+from webkitpy.layout_tests.servers import http_server_base
+
+
+_log = logging.getLogger(__name__)
+
+
+class Lighttpd(http_server_base.HttpServerBase):
+
+    def __init__(self, port_obj, output_dir, background=False, port=None,
+                 root=None, run_background=None, additional_dirs=None,
+                 layout_tests_dir=None, number_of_servers=None):
+        """Args:
+          output_dir: the absolute path to the layout test result directory
+        """
+        # Webkit tests
+        http_server_base.HttpServerBase.__init__(self, port_obj, number_of_servers)
+        self._name = 'lighttpd'
+        self._output_dir = output_dir
+        self._port = port
+        self._root = root
+        self._run_background = run_background
+        self._additional_dirs = additional_dirs
+        self._layout_tests_dir = layout_tests_dir
+
+        self._pid_file = self._filesystem.join(self._runtime_path, '%s.pid' % self._name)
+
+        if self._port:
+            self._port = int(self._port)
+
+        if not self._layout_tests_dir:
+            self._layout_tests_dir = self._port_obj.layout_tests_dir()
+
+        self._webkit_tests = os.path.join(self._layout_tests_dir, 'http', 'tests')
+        self._js_test_resource = os.path.join(self._layout_tests_dir, 'fast', 'js', 'resources')
+        self._media_resource = os.path.join(self._layout_tests_dir, 'media')
+
+        # Self generated certificate for SSL server (for client cert get
+        # <base-path>\chrome\test\data\ssl\certs\root_ca_cert.crt)
+        self._pem_file = os.path.join(
+            os.path.dirname(os.path.abspath(__file__)), 'httpd2.pem')
+
+        # One mapping where we can get to everything
+        self.VIRTUALCONFIG = []
+
+        if self._webkit_tests:
+            self.VIRTUALCONFIG.extend(
+               # Three mappings (one with SSL) for LayoutTests http tests
+               [{'port': 8000, 'docroot': self._webkit_tests},
+                {'port': 8080, 'docroot': self._webkit_tests},
+                {'port': 8443, 'docroot': self._webkit_tests,
+                 'sslcert': self._pem_file}])
+
+    def _prepare_config(self):
+        base_conf_file = self._port_obj.path_from_webkit_base('Tools',
+            'Scripts', 'webkitpy', 'layout_tests', 'servers', 'lighttpd.conf')
+        out_conf_file = os.path.join(self._output_dir, 'lighttpd.conf')
+        time_str = time.strftime("%d%b%Y-%H%M%S")
+        access_file_name = "access.log-" + time_str + ".txt"
+        access_log = os.path.join(self._output_dir, access_file_name)
+        log_file_name = "error.log-" + time_str + ".txt"
+        error_log = os.path.join(self._output_dir, log_file_name)
+
+        # Write out the config
+        base_conf = self._filesystem.read_text_file(base_conf_file)
+
+        # FIXME: This should be re-worked so that this block can
+        # use with open() instead of a manual file.close() call.
+        f = self._filesystem.open_text_file_for_writing(out_conf_file)
+        f.write(base_conf)
+
+        # Write out our cgi handlers.  Run perl through env so that it
+        # processes the #! line and runs perl with the proper command
+        # line arguments. Emulate apache's mod_asis with a cat cgi handler.
+        f.write(('cgi.assign = ( ".cgi"  => "/usr/bin/env",\n'
+                 '               ".pl"   => "/usr/bin/env",\n'
+                 '               ".asis" => "/bin/cat",\n'
+                 '               ".php"  => "%s" )\n\n') %
+                                     self._port_obj._path_to_lighttpd_php())
+
+        # Setup log files
+        f.write(('server.errorlog = "%s"\n'
+                 'accesslog.filename = "%s"\n\n') % (error_log, access_log))
+
+        # Setup upload folders. Upload folder is to hold temporary upload files
+        # and also POST data. This is used to support XHR layout tests that
+        # does POST.
+        f.write(('server.upload-dirs = ( "%s" )\n\n') % (self._output_dir))
+
+        # Setup a link to where the js test templates are stored
+        f.write(('alias.url = ( "/js-test-resources" => "%s" )\n\n') %
+                    (self._js_test_resource))
+
+        if self._additional_dirs:
+            for alias, path in self._additional_dirs.iteritems():
+                f.write(('alias.url += ( "%s" => "%s" )\n\n') % (alias, path))
+
+        # Setup a link to where the media resources are stored.
+        f.write(('alias.url += ( "/media-resources" => "%s" )\n\n') %
+                    (self._media_resource))
+
+        # dump out of virtual host config at the bottom.
+        if self._root:
+            if self._port:
+                # Have both port and root dir.
+                mappings = [{'port': self._port, 'docroot': self._root}]
+            else:
+                # Have only a root dir - set the ports as for LayoutTests.
+                # This is used in ui_tests to run http tests against a browser.
+
+                # default set of ports as for LayoutTests but with a
+                # specified root.
+                mappings = [{'port': 8000, 'docroot': self._root},
+                            {'port': 8080, 'docroot': self._root},
+                            {'port': 8443, 'docroot': self._root,
+                             'sslcert': self._pem_file}]
+        else:
+            mappings = self.VIRTUALCONFIG
+        for mapping in mappings:
+            ssl_setup = ''
+            if 'sslcert' in mapping:
+                ssl_setup = ('  ssl.engine = "enable"\n'
+                             '  ssl.pemfile = "%s"\n' % mapping['sslcert'])
+
+            f.write(('$SERVER["socket"] == "127.0.0.1:%d" {\n'
+                     '  server.document-root = "%s"\n' +
+                     ssl_setup +
+                     '}\n\n') % (mapping['port'], mapping['docroot']))
+        f.close()
+
+        executable = self._port_obj._path_to_lighttpd()
+        module_path = self._port_obj._path_to_lighttpd_modules()
+        start_cmd = [executable,
+                     # Newly written config file
+                     '-f', os.path.join(self._output_dir, 'lighttpd.conf'),
+                     # Where it can find its module dynamic libraries
+                     '-m', module_path]
+
+        if not self._run_background:
+            start_cmd.append(# Don't background
+                             '-D')
+
+        # Copy liblightcomp.dylib to /tmp/lighttpd/lib to work around the
+        # bug that mod_alias.so loads it from the hard coded path.
+        if self._port_obj.host.platform.is_mac():
+            tmp_module_path = '/tmp/lighttpd/lib'
+            if not self._filesystem.exists(tmp_module_path):
+                self._filesystem.maybe_make_directory(tmp_module_path)
+            lib_file = 'liblightcomp.dylib'
+            self._filesystem.copyfile(self._filesystem.join(module_path, lib_file),
+                                      self._filesystem.join(tmp_module_path, lib_file))
+
+        self._start_cmd = start_cmd
+        self._env = self._port_obj.setup_environ_for_server('lighttpd')
+        self._mappings = mappings
+
+    def _remove_stale_logs(self):
+        # Sometimes logs are open in other processes but they should clear eventually.
+        for log_prefix in ('access.log-', 'error.log-'):
+            try:
+                self._remove_log_files(self._output_dir, log_prefix)
+            except OSError, e:
+                _log.warning('Failed to remove old %s %s files' % (self._name, log_prefix))
+
+    def _spawn_process(self):
+        _log.debug('Starting %s server, cmd="%s"' % (self._name, self._start_cmd))
+        process = self._executive.popen(self._start_cmd, env=self._env, shell=False, stderr=self._executive.PIPE)
+        pid = process.pid
+        self._filesystem.write_text_file(self._pid_file, str(pid))
+        return pid
+
+    def _stop_running_server(self):
+        # FIXME: It would be nice if we had a cleaner way of killing this process.
+        # Currently we throw away the process object created in _spawn_process,
+        # since there doesn't appear to be any way to kill the server any more
+        # cleanly using it than just killing the pid, and we need to support
+        # killing a pid directly anyway for run-webkit-httpd and run-webkit-websocketserver.
+        self._wait_for_action(self._check_and_kill)
+        if self._filesystem.exists(self._pid_file):
+            self._filesystem.remove(self._pid_file)
+
+    def _check_and_kill(self):
+        if self._executive.check_running_pid(self._pid):
+            self._executive.kill_process(self._pid)
+            return False
+        return True
diff --git a/Tools/Scripts/webkitpy/layout_tests/servers/http_server_base.py b/Tools/Scripts/webkitpy/layout_tests/servers/http_server_base.py
new file mode 100755
index 0000000..c1c3da8
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/servers/http_server_base.py
@@ -0,0 +1,202 @@
+#!/usr/bin/env python
+# Copyright (C) 2011 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.
+
+"""Base class with common routines between the Apache, Lighttpd, and websocket servers."""
+
+import errno
+import logging
+import socket
+import sys
+import tempfile
+import time
+
+
+_log = logging.getLogger(__name__)
+
+
+class ServerError(Exception):
+    pass
+
+
+class HttpServerBase(object):
+    """A skeleton class for starting and stopping servers used by the layout tests."""
+
+    def __init__(self, port_obj, number_of_servers=None):
+        self._executive = port_obj._executive
+        self._filesystem = port_obj._filesystem
+        self._name = '<virtual>'
+        self._mappings = {}
+        self._pid = None
+        self._pid_file = None
+        self._port_obj = port_obj
+        self._number_of_servers = number_of_servers
+
+        # We need a non-checkout-dependent place to put lock files, etc. We
+        # don't use the Python default on the Mac because it defaults to a
+        # randomly-generated directory under /var/folders and no one would ever
+        # look there.
+        tmpdir = tempfile.gettempdir()
+        if port_obj.host.platform.is_mac():
+            tmpdir = '/tmp'
+
+        self._runtime_path = self._filesystem.join(tmpdir, "WebKit")
+        self._filesystem.maybe_make_directory(self._runtime_path)
+
+    def start(self):
+        """Starts the server. It is an error to start an already started server.
+
+        This method also stops any stale servers started by a previous instance."""
+        assert not self._pid, '%s server is already running' % self._name
+
+        # Stop any stale servers left over from previous instances.
+        if self._filesystem.exists(self._pid_file):
+            self._pid = int(self._filesystem.read_text_file(self._pid_file))
+            self._stop_running_server()
+            self._pid = None
+
+        self._remove_stale_logs()
+        self._prepare_config()
+        self._check_that_all_ports_are_available()
+
+        self._pid = self._spawn_process()
+
+        if self._wait_for_action(self._is_server_running_on_all_ports):
+            _log.debug("%s successfully started (pid = %d)" % (self._name, self._pid))
+        else:
+            self._stop_running_server()
+            raise ServerError('Failed to start %s server' % self._name)
+
+    def stop(self):
+        """Stops the server. Stopping a server that isn't started is harmless."""
+        actual_pid = None
+        if self._filesystem.exists(self._pid_file):
+            actual_pid = int(self._filesystem.read_text_file(self._pid_file))
+            if not self._pid:
+                self._pid = actual_pid
+
+        if not self._pid:
+            return
+
+        if not actual_pid:
+            _log.warning('Failed to stop %s: pid file is missing' % self._name)
+            return
+        if self._pid != actual_pid:
+            _log.warning('Failed to stop %s: pid file contains %d, not %d' %
+                         (self._name, actual_pid, self._pid))
+            # Try to kill the existing pid, anyway, in case it got orphaned.
+            self._executive.kill_process(self._pid)
+            self._pid = None
+            return
+
+        _log.debug("Attempting to shut down %s server at pid %d" % (self._name, self._pid))
+        self._stop_running_server()
+        _log.debug("%s server at pid %d stopped" % (self._name, self._pid))
+        self._pid = None
+
+    def _prepare_config(self):
+        """This routine can be overridden by subclasses to do any sort
+        of initialization required prior to starting the server that may fail."""
+        pass
+
+    def _remove_stale_logs(self):
+        """This routine can be overridden by subclasses to try and remove logs
+        left over from a prior run. This routine should log warnings if the
+        files cannot be deleted, but should not fail unless failure to
+        delete the logs will actually cause start() to fail."""
+        pass
+
+    def _spawn_process(self):
+        """This routine must be implemented by subclasses to actually start the server.
+
+        This routine returns the pid of the started process, and also ensures that that
+        pid has been written to self._pid_file."""
+        raise NotImplementedError()
+
+    def _stop_running_server(self):
+        """This routine must be implemented by subclasses to actually stop the running server listed in self._pid_file."""
+        raise NotImplementedError()
+
+    # Utility routines.
+
+    def _remove_log_files(self, folder, starts_with):
+        files = self._filesystem.listdir(folder)
+        for file in files:
+            if file.startswith(starts_with):
+                full_path = self._filesystem.join(folder, file)
+                self._filesystem.remove(full_path)
+
+    def _wait_for_action(self, action, wait_secs=20.0, sleep_secs=1.0):
+        """Repeat the action for wait_sec or until it succeeds, sleeping for sleep_secs
+        in between each attempt. Returns whether it succeeded."""
+        start_time = time.time()
+        while time.time() - start_time < wait_secs:
+            if action():
+                return True
+            _log.debug("Waiting for action: %s" % action)
+            time.sleep(sleep_secs)
+
+        return False
+
+    def _is_server_running_on_all_ports(self):
+        """Returns whether the server is running on all the desired ports."""
+        if not self._executive.check_running_pid(self._pid):
+            _log.debug("Server isn't running at all")
+            raise ServerError("Server exited")
+
+        for mapping in self._mappings:
+            s = socket.socket()
+            port = mapping['port']
+            try:
+                s.connect(('localhost', port))
+                _log.debug("Server running on %d" % port)
+            except IOError, e:
+                if e.errno not in (errno.ECONNREFUSED, errno.ECONNRESET):
+                    raise
+                _log.debug("Server NOT running on %d: %s" % (port, e))
+                return False
+            finally:
+                s.close()
+        return True
+
+    def _check_that_all_ports_are_available(self):
+        for mapping in self._mappings:
+            s = socket.socket()
+            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+            port = mapping['port']
+            try:
+                s.bind(('localhost', port))
+            except IOError, e:
+                if e.errno in (errno.EALREADY, errno.EADDRINUSE):
+                    raise ServerError('Port %d is already in use.' % port)
+                elif sys.platform == 'win32' and e.errno in (errno.WSAEACCES,):
+                    raise ServerError('Port %d is already in use.' % port)
+                else:
+                    raise
+            finally:
+                s.close()
diff --git a/Tools/Scripts/webkitpy/layout_tests/servers/http_server_integrationtest.py b/Tools/Scripts/webkitpy/layout_tests/servers/http_server_integrationtest.py
new file mode 100755
index 0000000..237d689
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/servers/http_server_integrationtest.py
@@ -0,0 +1,145 @@
+# Copyright (C) 2011 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.
+
+"""Integration tests for the new-run-webkit-httpd and new-run-webkit-websocketserver scripts"""
+
+# FIXME: Rename this file to something more descriptive.
+
+import errno
+import os
+import socket
+import subprocess
+import sys
+import tempfile
+import unittest
+
+
+class BaseTest(unittest.TestCase):
+    """Basic framework for script tests."""
+    HOST = 'localhost'
+
+    # Override in actual test classes.
+    PORTS = None
+    SCRIPT_NAME = None
+
+    def assert_servers_are_down(self, ports=None):
+        ports = ports or self.PORTS
+        for port in ports:
+            try:
+                test_socket = socket.socket()
+                test_socket.connect((self.HOST, port))
+                self.fail()
+            except IOError, e:
+                self.assertTrue(e.errno in (errno.ECONNREFUSED, errno.ECONNRESET))
+            finally:
+                test_socket.close()
+
+    def assert_servers_are_up(self, ports=None):
+        ports = ports or self.PORTS
+        for port in ports:
+            try:
+                test_socket = socket.socket()
+                test_socket.connect((self.HOST, port))
+            except IOError, e:
+                self.fail('failed to connect to %s:%d' % (self.HOST, port))
+            finally:
+                test_socket.close()
+
+    def run_script(self, args):
+        script_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
+        script_path = os.path.join(script_dir, self.SCRIPT_NAME)
+        return subprocess.call([sys.executable, script_path] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+    def integration_test_server__normal(self):
+        if not self.SCRIPT_NAME:
+            return
+
+        self.assert_servers_are_down()
+        self.assertEquals(self.run_script(['--server', 'start']), 0)
+        self.assert_servers_are_up()
+        self.assertEquals(self.run_script(['--server', 'stop']), 0)
+        self.assert_servers_are_down()
+
+    def integration_test_server__fails(self):
+        if not self.SCRIPT_NAME:
+            return
+
+        # Test that if a port isn't available, the call fails.
+        for port_number in self.PORTS:
+            test_socket = socket.socket()
+            try:
+                try:
+                    test_socket.bind((self.HOST, port_number))
+                except socket.error, e:
+                    if e.errno in (errno.EADDRINUSE, errno.EALREADY):
+                        self.fail('could not bind to port %d: %s' % (port_number, str(e)))
+                    raise
+                self.assertEquals(self.run_script(['--server', 'start']), 1)
+            finally:
+                self.run_script(['--server', 'stop'])
+                test_socket.close()
+
+        # Test that calling stop() twice is harmless.
+        self.assertEquals(self.run_script(['--server', 'stop']), 0)
+
+    def maybe_make_dir(self, *comps):
+        try:
+            os.makedirs(os.path.join(*comps))
+        except OSError, e:
+            if e.errno != errno.EEXIST:
+                raise
+
+    def integration_test_port_and_root(self):
+        if not self.SCRIPT_NAME:
+            return
+
+        tmpdir = tempfile.mkdtemp(prefix='webkitpytest')
+        self.maybe_make_dir(tmpdir, 'http', 'tests', 'websocket')
+        self.maybe_make_dir(tmpdir, 'fast', 'js', 'resources')
+        self.maybe_make_dir(tmpdir, 'media')
+
+        self.assert_servers_are_down([18000])
+        self.assertEquals(self.run_script(['--server', 'start', '--port=18000', '--root', tmpdir]), 0)
+        self.assert_servers_are_up([18000])
+        self.assertEquals(self.run_script(['--server', 'stop']), 0)
+        self.assert_servers_are_down([18000])
+
+
+class HTTPServerTest(BaseTest):
+    """Tests that new-run-webkit-http must pass."""
+
+    PORTS = (8000, 8080, 8443)
+    SCRIPT_NAME = 'new-run-webkit-httpd'
+
+
+class WebsocketserverTest(BaseTest):
+    """Tests that new-run-webkit-websocketserver must pass."""
+
+    # FIXME: test TLS at some point?
+    PORTS = (8880, )
+    SCRIPT_NAME = 'new-run-webkit-websocketserver'
diff --git a/Tools/Scripts/webkitpy/layout_tests/servers/http_server_unittest.py b/Tools/Scripts/webkitpy/layout_tests/servers/http_server_unittest.py
new file mode 100644
index 0000000..7a14526
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/servers/http_server_unittest.py
@@ -0,0 +1,64 @@
+# Copyright (C) 2012 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 re
+import sys
+
+from webkitpy.common.host_mock import MockHost
+from webkitpy.layout_tests.port import test
+from webkitpy.layout_tests.servers.http_server import Lighttpd
+from webkitpy.layout_tests.servers.http_server_base import ServerError
+
+
+class TestHttpServer(unittest.TestCase):
+    def test_start_cmd(self):
+        # Fails on win - see https://bugs.webkit.org/show_bug.cgi?id=84726
+        if sys.platform in ('cygwin', 'win32'):
+            return
+
+        host = MockHost()
+        test_port = test.TestPort(host)
+        host.filesystem.write_text_file(
+            "/mock-checkout/Tools/Scripts/webkitpy/layout_tests/servers/lighttpd.conf", "Mock Config\n")
+        host.filesystem.write_text_file(
+            "/usr/lib/lighttpd/liblightcomp.dylib", "Mock dylib")
+
+        server = Lighttpd(test_port, "/mock/output_dir",
+                          additional_dirs={
+                              "/mock/one-additional-dir": "/mock-checkout/one-additional-dir",
+                              "/mock/another-additional-dir": "/mock-checkout/one-additional-dir"})
+        self.assertRaises(ServerError, server.start)
+
+        config_file = host.filesystem.read_text_file("/mock/output_dir/lighttpd.conf")
+        self.assertEquals(re.findall(r"alias.url.+", config_file), [
+            'alias.url = ( "/js-test-resources" => "/test.checkout/LayoutTests/fast/js/resources" )',
+            'alias.url += ( "/mock/one-additional-dir" => "/mock-checkout/one-additional-dir" )',
+            'alias.url += ( "/mock/another-additional-dir" => "/mock-checkout/one-additional-dir" )',
+            'alias.url += ( "/media-resources" => "/test.checkout/LayoutTests/media" )',
+        ])
diff --git a/Tools/Scripts/webkitpy/layout_tests/servers/httpd2.pem b/Tools/Scripts/webkitpy/layout_tests/servers/httpd2.pem
new file mode 100644
index 0000000..6349b78
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/servers/httpd2.pem
@@ -0,0 +1,41 @@
+-----BEGIN CERTIFICATE-----
+MIIEZDCCAkygAwIBAgIBATANBgkqhkiG9w0BAQUFADBgMRAwDgYDVQQDEwdUZXN0
+IENBMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN
+TW91bnRhaW4gVmlldzESMBAGA1UEChMJQ2VydCBUZXN0MB4XDTA4MDcyODIyMzIy
+OFoXDTEzMDcyNzIyMzIyOFowSjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlm
+b3JuaWExEjAQBgNVBAoTCUNlcnQgVGVzdDESMBAGA1UEAxMJMTI3LjAuMC4xMIGf
+MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQj2tPWPUgbuI4H3/3dnttqVbndwU3
+3BdRCd67DFM44GRrsjDSH4bY/EbFyX9D52d/iy6ZaAmDePcCz5k/fgP3DMujykYG
+qgNiV2ywxTlMj7NlN2C7SRt68fQMZr5iI7rypdxuaZt9lSMD3ENBffYtuLTyZd9a
+3JPJe1TaIab5GwIDAQABo4HCMIG/MAkGA1UdEwQCMAAwHQYDVR0OBBYEFCYLBv5K
+x5sLNVlpLh5FwTwhdDl7MIGSBgNVHSMEgYowgYeAFF3Of5nj1BlBMU/Gz7El9Vqv
+45cxoWSkYjBgMRAwDgYDVQQDEwdUZXN0IENBMQswCQYDVQQGEwJVUzETMBEGA1UE
+CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzESMBAGA1UEChMJ
+Q2VydCBUZXN0ggkA1FGT1D/e2U4wDQYJKoZIhvcNAQEFBQADggIBAEtkVmLObUgk
+b2cIA2S+QDtifq1UgVfBbytvR2lFmnADOR55mo0gHQG3HHqq4g034LmoVXDHhUk8
+Gb6aFiv4QubmVhLXcUelTRXwiNvGzkW7pC6Jrq105hdPjzXMKTcmiLaopm5Fqfc7
+hj5Cn1Sjspc8pdeQjrbeMdvca7KlFrGP8YkwCU2xOOX9PiN9G0966BWfjnr/fZZp
++OQVuUFHdiAZwthEMuDpAAXHqYXIsermgdOpgJaA53cf8NqBV2QGhtFgtsJCRoiu
+7DKqhyRWBGyz19VIH2b7y+6qvQVxuHk19kKRM0nftw/yNcJnm7gtttespMUPsOMa
+a2SD1G0hm0TND6vxaBhgR3cVqpl/qIpAdFi00Tm7hTyYE7I43zPW03t+/DpCt3Um
+EMRZsQ90co5q+bcx/vQ7YAtwUh30uMb0wpibeyCwDp8cqNmSiRkEuc/FjTYes5t8
+5gR//WX1l0+qjrjusO9NmoLnq2Yk6UcioX+z+q6Z/dudGfqhLfeWD2Q0LWYA242C
+d7km5Y3KAt1PJdVsof/aiVhVdddY/OIEKTRQhWEdDbosy2eh16BCKXT2FFvhNDg1
+AYFvn6I8nj9IldMJiIc3DdhacEAEzRMeRgPdzAa1griKUGknxsyTyRii8ru0WS6w
+DCNrlDOVXdzYGEZooBI76BDVY0W0akjV
+-----END CERTIFICATE-----
+-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQDQj2tPWPUgbuI4H3/3dnttqVbndwU33BdRCd67DFM44GRrsjDS
+H4bY/EbFyX9D52d/iy6ZaAmDePcCz5k/fgP3DMujykYGqgNiV2ywxTlMj7NlN2C7
+SRt68fQMZr5iI7rypdxuaZt9lSMD3ENBffYtuLTyZd9a3JPJe1TaIab5GwIDAQAB
+AoGANHXu8z2YIzlhE+bwhGm8MGBpKL3qhRuKjeriqMA36tWezOw8lY4ymEAU+Ulv
+BsCdaxqydQoTYou57m4TyUHEcxq9pq3H0zB0qL709DdHi/t4zbV9XIoAzC5v0/hG
+9+Ca29TwC02FCw+qLkNrtwCpwOcQmc+bPxqvFu1iMiahURECQQD2I/Hi2413CMZz
+TBjl8fMiVO9GhA2J0sc8Qi+YcgJakaLD9xcbaiLkTzPZDlA389C1b6Ia+poAr4YA
+Ve0FFbxpAkEA2OobayyHE/QtPEqoy6NLR57jirmVBNmSWWd4lAyL5UIHIYVttJZg
+8CLvbzaU/iDGwR+wKsM664rKPHEmtlyo4wJBAMeSqYO5ZOCJGu9NWjrHjM3fdAsG
+8zs2zhiLya+fcU0iHIksBW5TBmt71Jw/wMc9R5J1K0kYvFml98653O5si1ECQBCk
+RV4/mE1rmlzZzYFyEcB47DQkcM5ictvxGEsje0gnfKyRtAz6zI0f4QbDRUMJ+LWw
+XK+rMsYHa+SfOb0b9skCQQCLdeonsIpFDv/Uv+flHISy0WA+AFkLXrRkBKh6G/OD
+dMHaNevkJgUnpceVEnkrdenp5CcEoFTI17pd+nBgDm/B
+-----END RSA PRIVATE KEY-----
diff --git a/Tools/Scripts/webkitpy/layout_tests/servers/lighttpd.conf b/Tools/Scripts/webkitpy/layout_tests/servers/lighttpd.conf
new file mode 100644
index 0000000..4360c37
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/servers/lighttpd.conf
@@ -0,0 +1,89 @@
+server.tag                  = "LightTPD/1.4.19 (Win32)"
+server.modules              = ( "mod_accesslog",
+                                "mod_alias",
+                                "mod_cgi",
+                                "mod_rewrite" )
+
+# default document root required
+server.document-root = "."
+
+# files to check for if .../ is requested
+index-file.names            = ( "index.php", "index.pl", "index.cgi",
+                                "index.html", "index.htm", "default.htm" )
+# mimetype mapping
+mimetype.assign             = (
+  ".gif"          =>      "image/gif",
+  ".jpg"          =>      "image/jpeg",
+  ".jpeg"         =>      "image/jpeg",
+  ".png"          =>      "image/png",
+  ".svg"          =>      "image/svg+xml",
+  ".css"          =>      "text/css",
+  ".html"         =>      "text/html",
+  ".htm"          =>      "text/html",
+  ".xhtml"        =>      "application/xhtml+xml",
+  ".js"           =>      "application/x-javascript",
+  ".log"          =>      "text/plain",
+  ".conf"         =>      "text/plain",
+  ".text"         =>      "text/plain",
+  ".txt"          =>      "text/plain",
+  ".dtd"          =>      "text/xml",
+  ".xml"          =>      "text/xml",
+  ".manifest"     =>      "text/cache-manifest",
+ )
+
+# Use the "Content-Type" extended attribute to obtain mime type if possible
+mimetype.use-xattr          = "enable"
+
+##
+# which extensions should not be handle via static-file transfer
+#
+# .php, .pl, .fcgi are most often handled by mod_fastcgi or mod_cgi
+static-file.exclude-extensions = ( ".php", ".pl", ".cgi" )
+
+server.bind = "localhost"
+server.port = 8001
+
+## virtual directory listings
+dir-listing.activate        = "enable"
+#dir-listing.encoding       = "iso-8859-2"
+#dir-listing.external-css   = "style/oldstyle.css"
+
+## enable debugging
+#debug.log-request-header   = "enable"
+#debug.log-response-header  = "enable"
+#debug.log-request-handling = "enable"
+#debug.log-file-not-found   = "enable"
+
+#### SSL engine
+#ssl.engine                 = "enable"
+#ssl.pemfile                = "server.pem"
+
+# Rewrite rule for utf-8 path test (LayoutTests/http/tests/uri/utf8-path.html)
+# See the apache rewrite rule at LayoutTests/http/tests/uri/intercept/.htaccess
+# Rewrite rule for LayoutTests/http/tests/appcache/cyrillic-uri.html.
+# See the apache rewrite rule at
+# LayoutTests/http/tests/appcache/resources/intercept/.htaccess
+url.rewrite-once = (
+  "^/uri/intercept/(.*)" => "/uri/resources/print-uri.php",
+  "^/appcache/resources/intercept/(.*)" => "/appcache/resources/print-uri.php"
+)
+
+# LayoutTests/http/tests/xmlhttprequest/response-encoding.html uses an htaccess
+# to override charset for reply2.txt, reply2.xml, and reply4.txt.
+$HTTP["url"] =~ "^/xmlhttprequest/resources/reply2.(txt|xml)" {
+  mimetype.assign = (
+    ".txt" => "text/plain; charset=windows-1251",
+    ".xml" => "text/xml; charset=windows-1251"
+  )
+}
+$HTTP["url"] =~ "^/xmlhttprequest/resources/reply4.txt" {
+  mimetype.assign = ( ".txt" => "text/plain; charset=koi8-r" )
+}
+
+# LayoutTests/http/tests/appcache/wrong-content-type.html uses an htaccess
+# to override mime type for wrong-content-type.manifest.
+$HTTP["url"] =~ "^/appcache/resources/wrong-content-type.manifest" {
+  mimetype.assign = ( ".manifest" => "text/plain" )
+}
+
+# Autogenerated test-specific config follows.
diff --git a/Tools/Scripts/webkitpy/layout_tests/servers/websocket_server.py b/Tools/Scripts/webkitpy/layout_tests/servers/websocket_server.py
new file mode 100644
index 0000000..93747f6
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/servers/websocket_server.py
@@ -0,0 +1,177 @@
+#!/usr/bin/env python
+# Copyright (C) 2011 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 class to help start/stop the PyWebSocket server used by layout tests."""
+
+import logging
+import os
+import sys
+import time
+
+from webkitpy.layout_tests.servers import http_server
+from webkitpy.layout_tests.servers import http_server_base
+
+_log = logging.getLogger(__name__)
+
+
+_WS_LOG_PREFIX = 'pywebsocket.ws.log-'
+_WSS_LOG_PREFIX = 'pywebsocket.wss.log-'
+
+
+_DEFAULT_WS_PORT = 8880
+_DEFAULT_WSS_PORT = 9323
+
+
+class PyWebSocket(http_server.Lighttpd):
+    def __init__(self, port_obj, output_dir, port=_DEFAULT_WS_PORT,
+                 root=None, use_tls=False,
+                 private_key=None, certificate=None, ca_certificate=None,
+                 pidfile=None):
+        """Args:
+          output_dir: the absolute path to the layout test result directory
+        """
+        http_server.Lighttpd.__init__(self, port_obj, output_dir,
+                                      port=_DEFAULT_WS_PORT,
+                                      root=root)
+        self._output_dir = output_dir
+        self._pid_file = pidfile
+        self._process = None
+
+        self._port = port
+        self._root = root
+        self._use_tls = use_tls
+
+        self._name = 'pywebsocket'
+        if self._use_tls:
+            self._name = 'pywebsocket_secure'
+
+        if private_key:
+            self._private_key = private_key
+        else:
+            self._private_key = self._pem_file
+        if certificate:
+            self._certificate = certificate
+        else:
+            self._certificate = self._pem_file
+        self._ca_certificate = ca_certificate
+        if self._port:
+            self._port = int(self._port)
+        self._wsin = None
+        self._wsout = None
+        self._mappings = [{'port': self._port}]
+
+        if not self._pid_file:
+            self._pid_file = self._filesystem.join(self._runtime_path, '%s.pid' % self._name)
+
+        # Webkit tests
+        # FIXME: This is the wrong way to detect if we're in Chrome vs. WebKit!
+        # The port objects are supposed to abstract this.
+        if self._root:
+            self._layout_tests = self._filesystem.abspath(self._root)
+            self._web_socket_tests = self._filesystem.abspath(self._filesystem.join(self._root, 'http', 'tests', 'websocket', 'tests'))
+        else:
+            try:
+                self._layout_tests = self._port_obj.layout_tests_dir()
+                self._web_socket_tests = self._filesystem.join(self._layout_tests, 'http', 'tests', 'websocket', 'tests')
+            except:
+                self._web_socket_tests = None
+
+        if self._use_tls:
+            self._log_prefix = _WSS_LOG_PREFIX
+        else:
+            self._log_prefix = _WS_LOG_PREFIX
+
+    def _prepare_config(self):
+        time_str = time.strftime('%d%b%Y-%H%M%S')
+        log_file_name = self._log_prefix + time_str
+        # FIXME: Doesn't Executive have a devnull, so that we don't have to use os.devnull directly?
+        self._wsin = open(os.devnull, 'r')
+
+        error_log = self._filesystem.join(self._output_dir, log_file_name + "-err.txt")
+        output_log = self._filesystem.join(self._output_dir, log_file_name + "-out.txt")
+        self._wsout = self._filesystem.open_text_file_for_writing(output_log)
+
+        from webkitpy.thirdparty import mod_pywebsocket
+        python_interp = sys.executable
+        # FIXME: Use self._filesystem.path_to_module(self.__module__) instead of __file__
+        # I think this is trying to get the chrome directory?  Doesn't the port object know that?
+        pywebsocket_base = self._filesystem.join(self._filesystem.dirname(self._filesystem.dirname(self._filesystem.dirname(self._filesystem.abspath(__file__)))), 'thirdparty')
+        pywebsocket_script = self._filesystem.join(pywebsocket_base, 'mod_pywebsocket', 'standalone.py')
+        start_cmd = [
+            python_interp, '-u', pywebsocket_script,
+            '--server-host', 'localhost',
+            '--port', str(self._port),
+            # FIXME: Don't we have a self._port_obj.layout_test_path?
+            '--document-root', self._filesystem.join(self._layout_tests, 'http', 'tests'),
+            '--scan-dir', self._web_socket_tests,
+            '--cgi-paths', '/websocket/tests',
+            '--log-file', error_log,
+        ]
+
+        handler_map_file = self._filesystem.join(self._web_socket_tests, 'handler_map.txt')
+        if self._filesystem.exists(handler_map_file):
+            _log.debug('Using handler_map_file: %s' % handler_map_file)
+            start_cmd.append('--websock-handlers-map-file')
+            start_cmd.append(handler_map_file)
+        else:
+            _log.warning('No handler_map_file found')
+
+        if self._use_tls:
+            start_cmd.extend(['-t', '-k', self._private_key,
+                              '-c', self._certificate])
+            if self._ca_certificate:
+                start_cmd.append('--ca-certificate')
+                start_cmd.append(self._ca_certificate)
+
+        self._start_cmd = start_cmd
+        server_name = self._filesystem.basename(pywebsocket_script)
+        self._env = self._port_obj.setup_environ_for_server(server_name)
+        self._env['PYTHONPATH'] = (pywebsocket_base + os.path.pathsep + self._env.get('PYTHONPATH', ''))
+
+    def _remove_stale_logs(self):
+        try:
+            self._remove_log_files(self._output_dir, self._log_prefix)
+        except OSError, e:
+            _log.warning('Failed to remove stale %s log files: %s' % (self._name, str(e)))
+
+    def _spawn_process(self):
+        _log.debug('Starting %s server, cmd="%s"' % (self._name, self._start_cmd))
+        self._process = self._executive.popen(self._start_cmd, env=self._env, shell=False, stdin=self._wsin, stdout=self._wsout, stderr=self._executive.STDOUT)
+        self._filesystem.write_text_file(self._pid_file, str(self._process.pid))
+        return self._process.pid
+
+    def _stop_running_server(self):
+        super(PyWebSocket, self)._stop_running_server()
+
+        if self._wsin:
+            self._wsin.close()
+            self._wsin = None
+        if self._wsout:
+            self._wsout.close()
+            self._wsout = None
diff --git a/Tools/Scripts/webkitpy/layout_tests/views/__init__.py b/Tools/Scripts/webkitpy/layout_tests/views/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/views/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/layout_tests/views/metered_stream.py b/Tools/Scripts/webkitpy/layout_tests/views/metered_stream.py
new file mode 100644
index 0000000..acea93e
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/views/metered_stream.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python
+# Copyright (C) 2010, 2012 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 logging
+import os
+import sys
+import time
+
+LOG_HANDLER_NAME = 'MeteredStreamLogHandler'
+
+
+class MeteredStream(object):
+    """
+    This class implements a stream wrapper that has 'meters' as well as
+    regular output. A 'meter' is a single line of text that can be erased
+    and rewritten repeatedly, without producing multiple lines of output. It
+    can be used to produce effects like progress bars.
+    """
+
+    @staticmethod
+    def _erasure(txt):
+        num_chars = len(txt)
+        return '\b' * num_chars + ' ' * num_chars + '\b' * num_chars
+
+    @staticmethod
+    def _ensure_newline(txt):
+        return txt if txt.endswith('\n') else txt + '\n'
+
+    def __init__(self, stream=None, verbose=False, logger=None, time_fn=None, pid=None, number_of_columns=None):
+        self._stream = stream or sys.stderr
+        self._verbose = verbose
+        self._time_fn = time_fn or time.time
+        self._pid = pid or os.getpid()
+        self._isatty = self._stream.isatty()
+        self._erasing = self._isatty and not verbose
+        self._last_partial_line = ''
+        self._last_write_time = 0.0
+        self._throttle_delay_in_secs = 0.066 if self._erasing else 10.0
+        self._number_of_columns = sys.maxint
+        if self._isatty and number_of_columns:
+            self._number_of_columns = number_of_columns
+
+        self._logger = logger
+        self._log_handler = None
+        if self._logger:
+            log_level = logging.DEBUG if verbose else logging.INFO
+            self._log_handler = _LogHandler(self)
+            self._log_handler.setLevel(log_level)
+            self._logger.addHandler(self._log_handler)
+
+    def __del__(self):
+        self.cleanup()
+
+    def cleanup(self):
+        if self._logger:
+            self._logger.removeHandler(self._log_handler)
+            self._log_handler = None
+
+    def write_throttled_update(self, txt):
+        now = self._time_fn()
+        if now - self._last_write_time >= self._throttle_delay_in_secs:
+            self.write_update(txt, now)
+
+    def write_update(self, txt, now=None):
+        self.write(txt, now)
+        if self._erasing:
+            self._last_partial_line = txt[txt.rfind('\n') + 1:]
+
+    def write(self, txt, now=None, pid=None):
+        now = now or self._time_fn()
+        pid = pid or self._pid
+        self._last_write_time = now
+        if self._last_partial_line:
+            self._erase_last_partial_line()
+        if self._verbose:
+            now_tuple = time.localtime(now)
+            msg = '%02d:%02d:%02d.%03d %d %s' % (now_tuple.tm_hour, now_tuple.tm_min, now_tuple.tm_sec, int((now * 1000) % 1000), pid, self._ensure_newline(txt))
+        elif self._isatty:
+            msg = txt
+        else:
+            msg = self._ensure_newline(txt)
+
+        self._stream.write(msg)
+
+    def writeln(self, txt, now=None, pid=None):
+        self.write(self._ensure_newline(txt), now, pid)
+
+    def _erase_last_partial_line(self):
+        num_chars = len(self._last_partial_line)
+        self._stream.write(self._erasure(self._last_partial_line))
+        self._last_partial_line = ''
+
+    def flush(self):
+        if self._last_partial_line:
+            self._stream.write('\n')
+            self._last_partial_line = ''
+            self._stream.flush()
+
+    def number_of_columns(self):
+        return self._number_of_columns
+
+
+class _LogHandler(logging.Handler):
+    def __init__(self, meter):
+        logging.Handler.__init__(self)
+        self._meter = meter
+        self.name = LOG_HANDLER_NAME
+
+    def emit(self, record):
+        self._meter.writeln(record.getMessage(), record.created, record.process)
diff --git a/Tools/Scripts/webkitpy/layout_tests/views/metered_stream_unittest.py b/Tools/Scripts/webkitpy/layout_tests/views/metered_stream_unittest.py
new file mode 100644
index 0000000..b388ec6
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/views/metered_stream_unittest.py
@@ -0,0 +1,158 @@
+#!/usr/bin/python
+# Copyright (C) 2010, 2012 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 logging
+import re
+import StringIO
+import unittest
+
+from webkitpy.layout_tests.views.metered_stream import MeteredStream
+
+
+class RegularTest(unittest.TestCase):
+    verbose = False
+    isatty = False
+
+    def setUp(self):
+        self.stream = StringIO.StringIO()
+        self.buflist = self.stream.buflist
+        self.stream.isatty = lambda: self.isatty
+
+        # configure a logger to test that log calls do normally get included.
+        self.logger = logging.getLogger(__name__)
+        self.logger.setLevel(logging.DEBUG)
+        self.logger.propagate = False
+
+        # add a dummy time counter for a default behavior.
+        self.times = range(10)
+
+        self.meter = MeteredStream(self.stream, self.verbose, self.logger, self.time_fn, 8675)
+
+    def tearDown(self):
+        if self.meter:
+            self.meter.cleanup()
+            self.meter = None
+
+    def time_fn(self):
+        return self.times.pop(0)
+
+    def test_logging_not_included(self):
+        # This tests that if we don't hand a logger to the MeteredStream,
+        # nothing is logged.
+        logging_stream = StringIO.StringIO()
+        handler = logging.StreamHandler(logging_stream)
+        root_logger = logging.getLogger()
+        orig_level = root_logger.level
+        root_logger.addHandler(handler)
+        root_logger.setLevel(logging.DEBUG)
+        try:
+            self.meter = MeteredStream(self.stream, self.verbose, None, self.time_fn, 8675)
+            self.meter.write_throttled_update('foo')
+            self.meter.write_update('bar')
+            self.meter.write('baz')
+            self.assertEquals(logging_stream.buflist, [])
+        finally:
+            root_logger.removeHandler(handler)
+            root_logger.setLevel(orig_level)
+
+    def _basic(self, times):
+        self.times = times
+        self.meter.write_update('foo')
+        self.meter.write_update('bar')
+        self.meter.write_throttled_update('baz')
+        self.meter.write_throttled_update('baz 2')
+        self.meter.writeln('done')
+        self.assertEquals(self.times, [])
+        return self.buflist
+
+    def test_basic(self):
+        buflist = self._basic([0, 1, 2, 13, 14])
+        self.assertEquals(buflist, ['foo\n', 'bar\n', 'baz 2\n', 'done\n'])
+
+    def _log_after_update(self):
+        self.meter.write_update('foo')
+        self.logger.info('bar')
+        return self.buflist
+
+    def test_log_after_update(self):
+        buflist = self._log_after_update()
+        self.assertEquals(buflist, ['foo\n', 'bar\n'])
+
+    def test_log_args(self):
+        self.logger.info('foo %s %d', 'bar', 2)
+        self.assertEquals(self.buflist, ['foo bar 2\n'])
+
+class TtyTest(RegularTest):
+    verbose = False
+    isatty = True
+
+    def test_basic(self):
+        buflist = self._basic([0, 1, 1.05, 1.1, 2])
+        self.assertEquals(buflist, ['foo',
+                                     MeteredStream._erasure('foo'), 'bar',
+                                     MeteredStream._erasure('bar'), 'baz 2',
+                                     MeteredStream._erasure('baz 2'), 'done\n'])
+
+    def test_log_after_update(self):
+        buflist = self._log_after_update()
+        self.assertEquals(buflist, ['foo',
+                                     MeteredStream._erasure('foo'), 'bar\n'])
+
+
+class VerboseTest(RegularTest):
+    isatty = False
+    verbose = True
+
+    def test_basic(self):
+        buflist = self._basic([0, 1, 2.1, 13, 14.1234])
+        # We don't bother to match the hours and minutes of the timestamp since
+        # the local timezone can vary and we can't set that portably and easily.
+        self.assertTrue(re.match('\d\d:\d\d:00.000 8675 foo\n', buflist[0]))
+        self.assertTrue(re.match('\d\d:\d\d:01.000 8675 bar\n', buflist[1]))
+        self.assertTrue(re.match('\d\d:\d\d:13.000 8675 baz 2\n', buflist[2]))
+        self.assertTrue(re.match('\d\d:\d\d:14.123 8675 done\n', buflist[3]))
+        self.assertEquals(len(buflist), 4)
+
+    def test_log_after_update(self):
+        buflist = self._log_after_update()
+        self.assertTrue(re.match('\d\d:\d\d:00.000 8675 foo\n', buflist[0]))
+
+        # The second argument should have a real timestamp and pid, so we just check the format.
+        self.assertTrue(re.match('\d\d:\d\d:\d\d.\d\d\d \d+ bar\n', buflist[1]))
+
+        self.assertEquals(len(buflist), 2)
+
+    def test_log_args(self):
+        self.logger.info('foo %s %d', 'bar', 2)
+        self.assertEquals(len(self.buflist), 1)
+        self.assertTrue(self.buflist[0].endswith('foo bar 2\n'))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/layout_tests/views/printing.py b/Tools/Scripts/webkitpy/layout_tests/views/printing.py
new file mode 100644
index 0000000..b7a9195
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/views/printing.py
@@ -0,0 +1,504 @@
+#!/usr/bin/env python
+# Copyright (C) 2010, 2012 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.
+
+"""Package that handles non-debug, non-file output for run-webkit-tests."""
+
+import math
+import optparse
+
+from webkitpy.tool import grammar
+from webkitpy.common.net import resultsjsonparser
+from webkitpy.layout_tests.models import test_expectations
+from webkitpy.layout_tests.models.test_expectations import TestExpectations, TestExpectationParser
+from webkitpy.layout_tests.views.metered_stream import MeteredStream
+
+
+NUM_SLOW_TESTS_TO_LOG = 10
+
+
+def print_options():
+    return [
+        optparse.make_option('-q', '--quiet', action='store_true', default=False,
+                             help='run quietly (errors, warnings, and progress only)'),
+        optparse.make_option('-v', '--verbose', action='store_true', default=False,
+                             help='print a summarized result for every test (one line per test)'),
+        optparse.make_option('--details', action='store_true', default=False,
+                             help='print detailed results for every test'),
+        optparse.make_option('--debug-rwt-logging', action='store_true', default=False,
+                             help='print timestamps and debug information for run-webkit-tests itself'),
+    ]
+
+
+class Printer(object):
+    """Class handling all non-debug-logging printing done by run-webkit-tests.
+
+    Printing from run-webkit-tests falls into two buckets: general or
+    regular output that is read only by humans and can be changed at any
+    time, and output that is parsed by buildbots (and humans) and hence
+    must be changed more carefully and in coordination with the buildbot
+    parsing code (in chromium.org's buildbot/master.chromium/scripts/master/
+    log_parser/webkit_test_command.py script).
+
+    By default the buildbot-parsed code gets logged to stdout, and regular
+    output gets logged to stderr."""
+    def __init__(self, port, options, regular_output, buildbot_output, logger=None):
+        self.num_completed = 0
+        self.num_tests = 0
+        self._port = port
+        self._options = options
+        self._buildbot_stream = buildbot_output
+        self._meter = MeteredStream(regular_output, options.debug_rwt_logging, logger=logger,
+                                    number_of_columns=self._port.host.platform.terminal_width())
+        self._running_tests = []
+        self._completed_tests = []
+
+    def cleanup(self):
+        self._meter.cleanup()
+
+    def __del__(self):
+        self.cleanup()
+
+    def print_config(self, results_directory):
+        self._print_default("Using port '%s'" % self._port.name())
+        self._print_default("Test configuration: %s" % self._port.test_configuration())
+        self._print_default("Placing test results in %s" % results_directory)
+
+        # FIXME: should these options be in printing_options?
+        if self._options.new_baseline:
+            self._print_default("Placing new baselines in %s" % self._port.baseline_path())
+
+        fs = self._port.host.filesystem
+        fallback_path = [fs.split(x)[1] for x in self._port.baseline_search_path()]
+        self._print_default("Baseline search path: %s -> generic" % " -> ".join(fallback_path))
+
+        self._print_default("Using %s build" % self._options.configuration)
+        if self._options.pixel_tests:
+            self._print_default("Pixel tests enabled")
+        else:
+            self._print_default("Pixel tests disabled")
+
+        self._print_default("Regular timeout: %s, slow test timeout: %s" %
+                  (self._options.time_out_ms, self._options.slow_time_out_ms))
+
+        self._print_default('Command line: ' + ' '.join(self._port.driver_cmd_line()))
+        self._print_default('')
+
+    def print_found(self, num_all_test_files, num_to_run, repeat_each, iterations):
+        num_unique_tests = num_to_run / (repeat_each * iterations)
+        found_str = 'Found %s; running %d' % (grammar.pluralize('test', num_all_test_files), num_unique_tests)
+        if repeat_each * iterations > 1:
+            found_str += ' (%d times each: --repeat-each=%d --iterations=%d)' % (repeat_each * iterations, repeat_each, iterations)
+        found_str += ', skipping %d' % (num_all_test_files - num_unique_tests)
+        self._print_default(found_str + '.')
+
+    def print_expected(self, result_summary, tests_with_result_type_callback):
+        self._print_expected_results_of_type(result_summary, test_expectations.PASS, "passes", tests_with_result_type_callback)
+        self._print_expected_results_of_type(result_summary, test_expectations.FAIL, "failures", tests_with_result_type_callback)
+        self._print_expected_results_of_type(result_summary, test_expectations.FLAKY, "flaky", tests_with_result_type_callback)
+        self._print_debug('')
+
+    def print_workers_and_shards(self, num_workers, num_shards, num_locked_shards):
+        driver_name = self._port.driver_name()
+        if num_workers == 1:
+            self._print_default("Running 1 %s over %s." % (driver_name, grammar.pluralize('shard', num_shards)))
+        else:
+            self._print_default("Running %d %ss in parallel over %d shards (%d locked)." %
+                (num_workers, driver_name, num_shards, num_locked_shards))
+        self._print_default('')
+
+    def _print_expected_results_of_type(self, result_summary, result_type, result_type_str, tests_with_result_type_callback):
+        tests = tests_with_result_type_callback(result_type)
+        now = result_summary.tests_by_timeline[test_expectations.NOW]
+        wontfix = result_summary.tests_by_timeline[test_expectations.WONTFIX]
+
+        # We use a fancy format string in order to print the data out in a
+        # nicely-aligned table.
+        fmtstr = ("Expect: %%5d %%-8s (%%%dd now, %%%dd wontfix)"
+                  % (self._num_digits(now), self._num_digits(wontfix)))
+        self._print_debug(fmtstr % (len(tests), result_type_str, len(tests & now), len(tests & wontfix)))
+
+    def _num_digits(self, num):
+        ndigits = 1
+        if len(num):
+            ndigits = int(math.log10(len(num))) + 1
+        return ndigits
+
+    def print_results(self, run_time, thread_timings, test_timings, individual_test_timings, result_summary, unexpected_results):
+        self._print_timing_statistics(run_time, thread_timings, test_timings, individual_test_timings, result_summary)
+        self._print_result_summary(result_summary)
+        self._print_one_line_summary(result_summary.total - result_summary.expected_skips,
+                                     result_summary.expected - result_summary.expected_skips,
+                                     result_summary.unexpected)
+        self._print_unexpected_results(unexpected_results)
+
+    def _print_timing_statistics(self, total_time, thread_timings,
+                                 directory_test_timings, individual_test_timings,
+                                 result_summary):
+        self._print_debug("Test timing:")
+        self._print_debug("  %6.2f total testing time" % total_time)
+        self._print_debug("")
+        self._print_debug("Thread timing:")
+        cuml_time = 0
+        for t in thread_timings:
+            self._print_debug("    %10s: %5d tests, %6.2f secs" % (t['name'], t['num_tests'], t['total_time']))
+            cuml_time += t['total_time']
+        self._print_debug("   %6.2f cumulative, %6.2f optimal" % (cuml_time, cuml_time / int(self._options.child_processes)))
+        self._print_debug("")
+
+        self._print_aggregate_test_statistics(individual_test_timings)
+        self._print_individual_test_times(individual_test_timings, result_summary)
+        self._print_directory_timings(directory_test_timings)
+
+    def _print_aggregate_test_statistics(self, individual_test_timings):
+        times_for_dump_render_tree = [test_stats.test_run_time for test_stats in individual_test_timings]
+        self._print_statistics_for_test_timings("PER TEST TIME IN TESTSHELL (seconds):", times_for_dump_render_tree)
+
+    def _print_individual_test_times(self, individual_test_timings, result_summary):
+        # Reverse-sort by the time spent in DumpRenderTree.
+        individual_test_timings.sort(lambda a, b: cmp(b.test_run_time, a.test_run_time))
+        num_printed = 0
+        slow_tests = []
+        timeout_or_crash_tests = []
+        unexpected_slow_tests = []
+        for test_tuple in individual_test_timings:
+            test_name = test_tuple.test_name
+            is_timeout_crash_or_slow = False
+            if test_name in result_summary.slow_tests:
+                is_timeout_crash_or_slow = True
+                slow_tests.append(test_tuple)
+
+            if test_name in result_summary.failures:
+                result = result_summary.results[test_name].type
+                if (result == test_expectations.TIMEOUT or
+                    result == test_expectations.CRASH):
+                    is_timeout_crash_or_slow = True
+                    timeout_or_crash_tests.append(test_tuple)
+
+            if (not is_timeout_crash_or_slow and num_printed < NUM_SLOW_TESTS_TO_LOG):
+                num_printed = num_printed + 1
+                unexpected_slow_tests.append(test_tuple)
+
+        self._print_debug("")
+        self._print_test_list_timing("%s slowest tests that are not marked as SLOW and did not timeout/crash:" %
+            NUM_SLOW_TESTS_TO_LOG, unexpected_slow_tests)
+        self._print_debug("")
+        self._print_test_list_timing("Tests marked as SLOW:", slow_tests)
+        self._print_debug("")
+        self._print_test_list_timing("Tests that timed out or crashed:", timeout_or_crash_tests)
+        self._print_debug("")
+
+    def _print_test_list_timing(self, title, test_list):
+        self._print_debug(title)
+        for test_tuple in test_list:
+            test_run_time = round(test_tuple.test_run_time, 1)
+            self._print_debug("  %s took %s seconds" % (test_tuple.test_name, test_run_time))
+
+    def _print_directory_timings(self, directory_test_timings):
+        timings = []
+        for directory in directory_test_timings:
+            num_tests, time_for_directory = directory_test_timings[directory]
+            timings.append((round(time_for_directory, 1), directory, num_tests))
+        timings.sort()
+
+        self._print_debug("Time to process slowest subdirectories:")
+        min_seconds_to_print = 10
+        for timing in timings:
+            if timing[0] > min_seconds_to_print:
+                self._print_debug("  %s took %s seconds to run %s tests." % (timing[1], timing[0], timing[2]))
+        self._print_debug("")
+
+    def _print_statistics_for_test_timings(self, title, timings):
+        self._print_debug(title)
+        timings.sort()
+
+        num_tests = len(timings)
+        if not num_tests:
+            return
+        percentile90 = timings[int(.9 * num_tests)]
+        percentile99 = timings[int(.99 * num_tests)]
+
+        if num_tests % 2 == 1:
+            median = timings[((num_tests - 1) / 2) - 1]
+        else:
+            lower = timings[num_tests / 2 - 1]
+            upper = timings[num_tests / 2]
+            median = (float(lower + upper)) / 2
+
+        mean = sum(timings) / num_tests
+
+        for timing in timings:
+            sum_of_deviations = math.pow(timing - mean, 2)
+
+        std_deviation = math.sqrt(sum_of_deviations / num_tests)
+        self._print_debug("  Median:          %6.3f" % median)
+        self._print_debug("  Mean:            %6.3f" % mean)
+        self._print_debug("  90th percentile: %6.3f" % percentile90)
+        self._print_debug("  99th percentile: %6.3f" % percentile99)
+        self._print_debug("  Standard dev:    %6.3f" % std_deviation)
+        self._print_debug("")
+
+    def _print_result_summary(self, result_summary):
+        if not self._options.debug_rwt_logging:
+            return
+
+        failed = result_summary.total_failures
+        total = result_summary.total - result_summary.expected_skips
+        passed = total - failed - result_summary.remaining
+        pct_passed = 0.0
+        if total > 0:
+            pct_passed = float(passed) * 100 / total
+
+        self._print_for_bot("=> Results: %d/%d tests passed (%.1f%%)" % (passed, total, pct_passed))
+        self._print_for_bot("")
+        self._print_result_summary_entry(result_summary, test_expectations.NOW, "Tests to be fixed")
+
+        self._print_for_bot("")
+        # FIXME: We should be skipping anything marked WONTFIX, so we shouldn't bother logging these stats.
+        self._print_result_summary_entry(result_summary, test_expectations.WONTFIX,
+            "Tests that will only be fixed if they crash (WONTFIX)")
+        self._print_for_bot("")
+
+    def _print_result_summary_entry(self, result_summary, timeline, heading):
+        total = len(result_summary.tests_by_timeline[timeline])
+        not_passing = (total -
+           len(result_summary.tests_by_expectation[test_expectations.PASS] &
+               result_summary.tests_by_timeline[timeline]))
+        self._print_for_bot("=> %s (%d):" % (heading, not_passing))
+
+        for result in TestExpectations.EXPECTATION_ORDER:
+            if result in (test_expectations.PASS, test_expectations.SKIP):
+                continue
+            results = (result_summary.tests_by_expectation[result] &
+                       result_summary.tests_by_timeline[timeline])
+            desc = TestExpectations.EXPECTATION_DESCRIPTIONS[result]
+            if not_passing and len(results):
+                pct = len(results) * 100.0 / not_passing
+                self._print_for_bot("  %5d %-24s (%4.1f%%)" % (len(results), desc, pct))
+
+    def _print_one_line_summary(self, total, expected, unexpected):
+        incomplete = total - expected - unexpected
+        incomplete_str = ''
+        if incomplete:
+            self._print_default("")
+            incomplete_str = " (%d didn't run)" % incomplete
+
+        if self._options.verbose or self._options.debug_rwt_logging or unexpected:
+            self.writeln("")
+
+        summary = ''
+        if unexpected == 0:
+            if expected == total:
+                if expected > 1:
+                    summary = "All %d tests ran as expected." % expected
+                else:
+                    summary = "The test ran as expected."
+            else:
+                summary = "%s ran as expected%s." % (grammar.pluralize('test', expected), incomplete_str)
+        else:
+            summary = "%s ran as expected, %d didn't%s:" % (grammar.pluralize('test', expected), unexpected, incomplete_str)
+
+        self._print_quiet(summary)
+        self._print_quiet("")
+
+    def _test_status_line(self, test_name, suffix):
+        format_string = '[%d/%d] %s%s'
+        status_line = format_string % (self.num_completed, self.num_tests, test_name, suffix)
+        if len(status_line) > self._meter.number_of_columns():
+            overflow_columns = len(status_line) - self._meter.number_of_columns()
+            ellipsis = '...'
+            if len(test_name) < overflow_columns + len(ellipsis) + 2:
+                # We don't have enough space even if we elide, just show the test filename.
+                fs = self._port.host.filesystem
+                test_name = fs.split(test_name)[1]
+            else:
+                new_length = len(test_name) - overflow_columns - len(ellipsis)
+                prefix = int(new_length / 2)
+                test_name = test_name[:prefix] + ellipsis + test_name[-(new_length - prefix):]
+        return format_string % (self.num_completed, self.num_tests, test_name, suffix)
+
+    def print_started_test(self, test_name):
+        self._running_tests.append(test_name)
+        if len(self._running_tests) > 1:
+            suffix = ' (+%d)' % (len(self._running_tests) - 1)
+        else:
+            suffix = ''
+        if self._options.verbose:
+            write = self._meter.write_update
+        else:
+            write = self._meter.write_throttled_update
+        write(self._test_status_line(test_name, suffix))
+
+    def print_finished_test(self, result, expected, exp_str, got_str):
+        self.num_completed += 1
+        test_name = result.test_name
+
+        result_message = self._result_message(result.type, result.failures, expected, self._options.verbose)
+
+        if self._options.details:
+            self._print_test_trace(result, exp_str, got_str)
+        elif (self._options.verbose and not self._options.debug_rwt_logging) or not expected:
+            self.writeln(self._test_status_line(test_name, result_message))
+        elif self.num_completed == self.num_tests:
+            self._meter.write_update('')
+        else:
+            if test_name == self._running_tests[0]:
+                self._completed_tests.insert(0, [test_name, result_message])
+            else:
+                self._completed_tests.append([test_name, result_message])
+
+            for test_name, result_message in self._completed_tests:
+                self._meter.write_throttled_update(self._test_status_line(test_name, result_message))
+            self._completed_tests = []
+        self._running_tests.remove(test_name)
+
+    def _result_message(self, result_type, failures, expected, verbose):
+        exp_string = ' unexpectedly' if not expected else ''
+        if result_type == test_expectations.PASS:
+            return ' passed%s' % exp_string
+        else:
+            return ' failed%s (%s)' % (exp_string, ', '.join(failure.message() for failure in failures))
+
+
+    def _print_test_trace(self, result, exp_str, got_str):
+        test_name = result.test_name
+        self._print_default(self._test_status_line(test_name, ''))
+
+        base = self._port.lookup_virtual_test_base(test_name)
+        if base:
+            args = ' '.join(self._port.lookup_virtual_test_args(test_name))
+            self._print_default(' base: %s' % base)
+            self._print_default(' args: %s' % args)
+
+        for extension in ('.txt', '.png', '.wav', '.webarchive'):
+            self._print_baseline(test_name, extension)
+
+        self._print_default('  exp: %s' % exp_str)
+        self._print_default('  got: %s' % got_str)
+        self._print_default(' took: %-.3f' % result.test_run_time)
+        self._print_default('')
+
+    def _print_baseline(self, test_name, extension):
+        baseline = self._port.expected_filename(test_name, extension)
+        if self._port._filesystem.exists(baseline):
+            relpath = self._port.relative_test_filename(baseline)
+        else:
+            relpath = '<none>'
+        self._print_default('  %s: %s' % (extension[1:], relpath))
+
+    def _print_unexpected_results(self, unexpected_results):
+        # Prints to the buildbot stream
+        passes = {}
+        flaky = {}
+        regressions = {}
+
+        def add_to_dict_of_lists(dict, key, value):
+            dict.setdefault(key, []).append(value)
+
+        def add_result(test, results, passes=passes, flaky=flaky, regressions=regressions):
+            actual = results['actual'].split(" ")
+            expected = results['expected'].split(" ")
+            if actual == ['PASS']:
+                if 'CRASH' in expected:
+                    add_to_dict_of_lists(passes, 'Expected to crash, but passed', test)
+                elif 'TIMEOUT' in expected:
+                    add_to_dict_of_lists(passes, 'Expected to timeout, but passed', test)
+                else:
+                    add_to_dict_of_lists(passes, 'Expected to fail, but passed', test)
+            elif len(actual) > 1:
+                # We group flaky tests by the first actual result we got.
+                add_to_dict_of_lists(flaky, actual[0], test)
+            else:
+                add_to_dict_of_lists(regressions, results['actual'], test)
+
+        resultsjsonparser.for_each_test(unexpected_results['tests'], add_result)
+
+        if len(passes) or len(flaky) or len(regressions):
+            self._print_for_bot("")
+        if len(passes):
+            for key, tests in passes.iteritems():
+                self._print_for_bot("%s: (%d)" % (key, len(tests)))
+                tests.sort()
+                for test in tests:
+                    self._print_for_bot("  %s" % test)
+                self._print_for_bot("")
+            self._print_for_bot("")
+
+        if len(flaky):
+            descriptions = TestExpectations.EXPECTATION_DESCRIPTIONS
+            for key, tests in flaky.iteritems():
+                result = TestExpectations.EXPECTATIONS[key.lower()]
+                self._print_for_bot("Unexpected flakiness: %s (%d)" % (descriptions[result], len(tests)))
+                tests.sort()
+
+                for test in tests:
+                    result = resultsjsonparser.result_for_test(unexpected_results['tests'], test)
+                    actual = result['actual'].split(" ")
+                    expected = result['expected'].split(" ")
+                    result = TestExpectations.EXPECTATIONS[key.lower()]
+                    # FIXME: clean this up once the old syntax is gone
+                    new_expectations_list = [TestExpectationParser._inverted_expectation_tokens[exp] for exp in list(set(actual) | set(expected))]
+                    self._print_for_bot("  %s [ %s ]" % (test, " ".join(new_expectations_list)))
+                self._print_for_bot("")
+            self._print_for_bot("")
+
+        if len(regressions):
+            descriptions = TestExpectations.EXPECTATION_DESCRIPTIONS
+            for key, tests in regressions.iteritems():
+                result = TestExpectations.EXPECTATIONS[key.lower()]
+                self._print_for_bot("Regressions: Unexpected %s (%d)" % (descriptions[result], len(tests)))
+                tests.sort()
+                for test in tests:
+                    self._print_for_bot("  %s [ %s ]" % (test, TestExpectationParser._inverted_expectation_tokens[key]))
+                self._print_for_bot("")
+
+        if len(unexpected_results['tests']) and self._options.debug_rwt_logging:
+            self._print_for_bot("%s" % ("-" * 78))
+
+    def _print_quiet(self, msg):
+        self.writeln(msg)
+
+    def _print_default(self, msg):
+        if not self._options.quiet:
+            self.writeln(msg)
+
+    def _print_debug(self, msg):
+        if self._options.debug_rwt_logging:
+            self.writeln(msg)
+
+    def _print_for_bot(self, msg):
+        self._buildbot_stream.write(msg + "\n")
+
+    def write_update(self, msg):
+        self._meter.write_update(msg)
+
+    def writeln(self, msg):
+        self._meter.writeln(msg)
+
+    def flush(self):
+        self._meter.flush()
diff --git a/Tools/Scripts/webkitpy/layout_tests/views/printing_unittest.py b/Tools/Scripts/webkitpy/layout_tests/views/printing_unittest.py
new file mode 100644
index 0000000..bc30092
--- /dev/null
+++ b/Tools/Scripts/webkitpy/layout_tests/views/printing_unittest.py
@@ -0,0 +1,293 @@
+#!/usr/bin/python
+# Copyright (C) 2010, 2012 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.
+
+"""Unit tests for printing.py."""
+
+import optparse
+import StringIO
+import sys
+import time
+import unittest
+
+from webkitpy.common.host_mock import MockHost
+
+from webkitpy.common.system import logtesting
+from webkitpy.layout_tests import port
+from webkitpy.layout_tests.controllers import manager
+from webkitpy.layout_tests.models import result_summary
+from webkitpy.layout_tests.models import test_expectations
+from webkitpy.layout_tests.models import test_failures
+from webkitpy.layout_tests.models import test_results
+from webkitpy.layout_tests.views import printing
+
+
+def get_options(args):
+    print_options = printing.print_options()
+    option_parser = optparse.OptionParser(option_list=print_options)
+    return option_parser.parse_args(args)
+
+
+class TestUtilityFunctions(unittest.TestCase):
+    def test_print_options(self):
+        options, args = get_options([])
+        self.assertTrue(options is not None)
+
+
+class  Testprinter(unittest.TestCase):
+    def assertEmpty(self, stream):
+        self.assertFalse(stream.getvalue())
+
+    def assertNotEmpty(self, stream):
+        self.assertTrue(stream.getvalue())
+
+    def assertWritten(self, stream, contents):
+        self.assertEquals(stream.buflist, contents)
+
+    def reset(self, stream):
+        stream.buflist = []
+        stream.buf = ''
+
+    def get_printer(self, args=None):
+        args = args or []
+        printing_options = printing.print_options()
+        option_parser = optparse.OptionParser(option_list=printing_options)
+        options, args = option_parser.parse_args(args)
+        host = MockHost()
+        self._port = host.port_factory.get('test', options)
+        nproc = 2
+
+        regular_output = StringIO.StringIO()
+        buildbot_output = StringIO.StringIO()
+        printer = printing.Printer(self._port, options, regular_output, buildbot_output)
+        return printer, regular_output, buildbot_output
+
+    def get_result(self, test_name, result_type=test_expectations.PASS, run_time=0):
+        failures = []
+        if result_type == test_expectations.TIMEOUT:
+            failures = [test_failures.FailureTimeout()]
+        elif result_type == test_expectations.CRASH:
+            failures = [test_failures.FailureCrash()]
+        return test_results.TestResult(test_name, failures=failures, test_run_time=run_time)
+
+    def get_result_summary(self, test_names, expectations_str):
+        port.test_expectations = lambda: expectations_str
+        port.test_expectations_overrides = lambda: None
+        expectations = test_expectations.TestExpectations(self._port, test_names)
+
+        rs = result_summary.ResultSummary(expectations, test_names, 1, set())
+        return test_names, rs, expectations
+
+    def test_configure_and_cleanup(self):
+        # This test verifies that calling cleanup repeatedly and deleting
+        # the object is safe.
+        printer, err, out = self.get_printer()
+        printer.cleanup()
+        printer.cleanup()
+        printer = None
+
+    def test_print_config(self):
+        printer, err, out = self.get_printer()
+        # FIXME: it's lame that i have to set these options directly.
+        printer._options.pixel_tests = True
+        printer._options.new_baseline = True
+        printer._options.time_out_ms = 6000
+        printer._options.slow_time_out_ms = 12000
+        printer.print_config('/tmp')
+        self.assertTrue("Using port 'test-mac-leopard'" in err.getvalue())
+        self.assertTrue('Test configuration: <leopard, x86, release>' in err.getvalue())
+        self.assertTrue('Placing test results in /tmp' in err.getvalue())
+        self.assertTrue('Baseline search path: test-mac-leopard -> test-mac-snowleopard -> generic' in err.getvalue())
+        self.assertTrue('Using Release build' in err.getvalue())
+        self.assertTrue('Pixel tests enabled' in err.getvalue())
+        self.assertTrue('Command line:' in err.getvalue())
+        self.assertTrue('Regular timeout: ' in err.getvalue())
+
+        self.reset(err)
+        printer._options.quiet = True
+        printer.print_config('/tmp')
+        self.assertFalse('Baseline search path: test-mac-leopard -> test-mac-snowleopard -> generic' in err.getvalue())
+
+    def test_print_one_line_summary(self):
+        printer, err, out = self.get_printer()
+        printer._print_one_line_summary(1, 1, 0)
+        self.assertWritten(err, ["The test ran as expected.\n", "\n"])
+
+        printer, err, out = self.get_printer()
+        printer._print_one_line_summary(1, 1, 0)
+        self.assertWritten(err, ["The test ran as expected.\n", "\n"])
+
+        printer, err, out = self.get_printer()
+        printer._print_one_line_summary(2, 1, 1)
+        self.assertWritten(err, ["\n", "1 test ran as expected, 1 didn't:\n", "\n"])
+
+        printer, err, out = self.get_printer()
+        printer._print_one_line_summary(3, 2, 1)
+        self.assertWritten(err, ["\n", "2 tests ran as expected, 1 didn't:\n", "\n"])
+
+        printer, err, out = self.get_printer()
+        printer._print_one_line_summary(3, 2, 0)
+        self.assertWritten(err, ['\n', "2 tests ran as expected (1 didn't run).\n", '\n'])
+
+    def test_print_unexpected_results(self):
+        # This routine is the only one that prints stuff that the bots
+        # care about.
+        #
+        # FIXME: there's some weird layering going on here. It seems
+        # like we shouldn't be both using an expectations string and
+        # having to specify whether or not the result was expected.
+        # This whole set of tests should probably be rewritten.
+        #
+        # FIXME: Plus, the fact that we're having to call into
+        # run_webkit_tests is clearly a layering inversion.
+        def get_unexpected_results(expected, passing, flaky):
+            """Return an unexpected results summary matching the input description.
+
+            There are a lot of different combinations of test results that
+            can be tested; this routine produces various combinations based
+            on the values of the input flags.
+
+            Args
+                expected: whether the tests ran as expected
+                passing: whether the tests should all pass
+                flaky: whether the tests should be flaky (if False, they
+                    produce the same results on both runs; if True, they
+                    all pass on the second run).
+
+            """
+            test_is_slow = False
+            paths, rs, exp = self.get_result_summary(tests, expectations)
+            if expected:
+                rs.add(self.get_result('passes/text.html', test_expectations.PASS), expected, test_is_slow)
+                rs.add(self.get_result('failures/expected/timeout.html', test_expectations.TIMEOUT), expected, test_is_slow)
+                rs.add(self.get_result('failures/expected/crash.html', test_expectations.CRASH), expected, test_is_slow)
+            elif passing:
+                rs.add(self.get_result('passes/text.html'), expected, test_is_slow)
+                rs.add(self.get_result('failures/expected/timeout.html'), expected, test_is_slow)
+                rs.add(self.get_result('failures/expected/crash.html'), expected, test_is_slow)
+            else:
+                rs.add(self.get_result('passes/text.html', test_expectations.TIMEOUT), expected, test_is_slow)
+                rs.add(self.get_result('failures/expected/timeout.html', test_expectations.CRASH), expected, test_is_slow)
+                rs.add(self.get_result('failures/expected/crash.html', test_expectations.TIMEOUT), expected, test_is_slow)
+            retry = rs
+            if flaky:
+                paths, retry, exp = self.get_result_summary(tests, expectations)
+                retry.add(self.get_result('passes/text.html'), True, test_is_slow)
+                retry.add(self.get_result('failures/expected/timeout.html'), True, test_is_slow)
+                retry.add(self.get_result('failures/expected/crash.html'), True, test_is_slow)
+            unexpected_results = manager.summarize_results(self._port, exp, rs, retry, test_timings={}, only_unexpected=True, interrupted=False)
+            return unexpected_results
+
+        tests = ['passes/text.html', 'failures/expected/timeout.html', 'failures/expected/crash.html']
+        expectations = ''
+
+        printer, err, out = self.get_printer()
+
+        # test everything running as expected
+        ur = get_unexpected_results(expected=True, passing=False, flaky=False)
+        printer._print_unexpected_results(ur)
+        self.assertEmpty(err)
+        self.assertEmpty(out)
+
+        # test failures
+        printer, err, out = self.get_printer()
+        ur = get_unexpected_results(expected=False, passing=False, flaky=False)
+        printer._print_unexpected_results(ur)
+        self.assertEmpty(err)
+        self.assertNotEmpty(out)
+
+        # test unexpected flaky
+        printer, err, out = self.get_printer()
+        ur = get_unexpected_results(expected=False, passing=False, flaky=True)
+        printer._print_unexpected_results(ur)
+        self.assertEmpty(err)
+        self.assertNotEmpty(out)
+
+        printer, err, out = self.get_printer()
+        ur = get_unexpected_results(expected=False, passing=False, flaky=False)
+        printer._print_unexpected_results(ur)
+        self.assertEmpty(err)
+        self.assertNotEmpty(out)
+
+        expectations = """
+BUGX : failures/expected/crash.html = CRASH
+BUGX : failures/expected/timeout.html = TIMEOUT
+"""
+        printer, err, out = self.get_printer()
+        ur = get_unexpected_results(expected=False, passing=False, flaky=False)
+        printer._print_unexpected_results(ur)
+        self.assertEmpty(err)
+        self.assertNotEmpty(out)
+
+        printer, err, out = self.get_printer()
+        ur = get_unexpected_results(expected=False, passing=True, flaky=False)
+        printer._print_unexpected_results(ur)
+        self.assertEmpty(err)
+        self.assertNotEmpty(out)
+
+    def test_print_unexpected_results_buildbot(self):
+        # FIXME: Test that print_unexpected_results() produces the printer the
+        # buildbot is expecting.
+        pass
+
+    def test_test_status_line(self):
+        printer, _, _ = self.get_printer()
+        printer._meter.number_of_columns = lambda: 80
+        actual = printer._test_status_line('fast/dom/HTMLFormElement/associated-elements-after-index-assertion-fail1.html', ' passed')
+        self.assertEquals(80, len(actual))
+        self.assertEquals(actual, '[0/0] fast/dom/HTMLFormElement/associa...after-index-assertion-fail1.html passed')
+
+        printer._meter.number_of_columns = lambda: 89
+        actual = printer._test_status_line('fast/dom/HTMLFormElement/associated-elements-after-index-assertion-fail1.html', ' passed')
+        self.assertEquals(89, len(actual))
+        self.assertEquals(actual, '[0/0] fast/dom/HTMLFormElement/associated-...ents-after-index-assertion-fail1.html passed')
+
+        printer._meter.number_of_columns = lambda: sys.maxint
+        actual = printer._test_status_line('fast/dom/HTMLFormElement/associated-elements-after-index-assertion-fail1.html', ' passed')
+        self.assertEquals(90, len(actual))
+        self.assertEquals(actual, '[0/0] fast/dom/HTMLFormElement/associated-elements-after-index-assertion-fail1.html passed')
+
+        printer._meter.number_of_columns = lambda: 18
+        actual = printer._test_status_line('fast/dom/HTMLFormElement/associated-elements-after-index-assertion-fail1.html', ' passed')
+        self.assertEquals(18, len(actual))
+        self.assertEquals(actual, '[0/0] f...l passed')
+
+        printer._meter.number_of_columns = lambda: 10
+        actual = printer._test_status_line('fast/dom/HTMLFormElement/associated-elements-after-index-assertion-fail1.html', ' passed')
+        self.assertEquals(actual, '[0/0] associated-elements-after-index-assertion-fail1.html passed')
+
+    def test_details(self):
+        printer, err, _ = self.get_printer(['--details'])
+        result = self.get_result('passes/image.html')
+        printer.print_started_test('passes/image.html')
+        printer.print_finished_test(result, expected=False, exp_str='', got_str='')
+        self.assertNotEmpty(err)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/performance_tests/__init__.py b/Tools/Scripts/webkitpy/performance_tests/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/performance_tests/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/performance_tests/perftest.py b/Tools/Scripts/webkitpy/performance_tests/perftest.py
new file mode 100644
index 0000000..9e2f87d
--- /dev/null
+++ b/Tools/Scripts/webkitpy/performance_tests/perftest.py
@@ -0,0 +1,392 @@
+#!/usr/bin/env python
+# Copyright (C) 2012 Google Inc. All rights reserved.
+# Copyright (C) 2012 Zoltan Horvath, Adobe Systems Incorporated. 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 errno
+import logging
+import math
+import re
+import os
+import signal
+import socket
+import subprocess
+import sys
+import time
+
+# Import for auto-install
+if sys.platform not in ('cygwin', 'win32'):
+    # FIXME: webpagereplay doesn't work on win32. See https://bugs.webkit.org/show_bug.cgi?id=88279.
+    import webkitpy.thirdparty.autoinstalled.webpagereplay.replay
+
+from webkitpy.layout_tests.controllers.test_result_writer import TestResultWriter
+from webkitpy.layout_tests.port.driver import DriverInput
+from webkitpy.layout_tests.port.driver import DriverOutput
+
+
+_log = logging.getLogger(__name__)
+
+
+class PerfTest(object):
+    def __init__(self, port, test_name, path_or_url):
+        self._port = port
+        self._test_name = test_name
+        self._path_or_url = path_or_url
+
+    def test_name(self):
+        return self._test_name
+
+    def path_or_url(self):
+        return self._path_or_url
+
+    def prepare(self, time_out_ms):
+        return True
+
+    def run(self, driver, time_out_ms):
+        output = self.run_single(driver, self.path_or_url(), time_out_ms)
+        if self.run_failed(output):
+            return None
+        return self.parse_output(output)
+
+    def run_single(self, driver, path_or_url, time_out_ms, should_run_pixel_test=False):
+        return driver.run_test(DriverInput(path_or_url, time_out_ms, image_hash=None, should_run_pixel_test=should_run_pixel_test), stop_when_done=False)
+
+    def run_failed(self, output):
+        if output.text == None or output.error:
+            pass
+        elif output.timeout:
+            _log.error('timeout: %s' % self.test_name())
+        elif output.crash:
+            _log.error('crash: %s' % self.test_name())
+        else:
+            return False
+
+        if output.error:
+            _log.error('error: %s\n%s' % (self.test_name(), output.error))
+
+        return True
+
+    _lines_to_ignore_in_parser_result = [
+        re.compile(r'^Running \d+ times$'),
+        re.compile(r'^Ignoring warm-up '),
+        re.compile(r'^Info:'),
+        re.compile(r'^\d+(.\d+)?(\s*(runs\/s|ms|fps))?$'),
+        # Following are for handle existing test like Dromaeo
+        re.compile(re.escape("""main frame - has 1 onunload handler(s)""")),
+        re.compile(re.escape("""frame "<!--framePath //<!--frame0-->-->" - has 1 onunload handler(s)""")),
+        re.compile(re.escape("""frame "<!--framePath //<!--frame0-->/<!--frame0-->-->" - has 1 onunload handler(s)""")),
+        # Following is for html5.html
+        re.compile(re.escape("""Blocked access to external URL http://www.whatwg.org/specs/web-apps/current-work/"""))]
+
+    def _should_ignore_line_in_parser_test_result(self, line):
+        if not line:
+            return True
+        for regex in self._lines_to_ignore_in_parser_result:
+            if regex.search(line):
+                return True
+        return False
+
+    _description_regex = re.compile(r'^Description: (?P<description>.*)$', re.IGNORECASE)
+    _result_classes = ['Time', 'JS Heap', 'Malloc']
+    _result_class_regex = re.compile(r'^(?P<resultclass>' + r'|'.join(_result_classes) + '):')
+    _statistics_keys = ['avg', 'median', 'stdev', 'min', 'max', 'unit', 'values']
+    _score_regex = re.compile(r'^(?P<key>' + r'|'.join(_statistics_keys) + r')\s+(?P<value>([0-9\.]+(,\s+)?)+)\s*(?P<unit>.*)')
+
+    def parse_output(self, output):
+        test_failed = False
+        results = {}
+        ordered_results_keys = []
+        test_name = re.sub(r'\.\w+$', '', self._test_name)
+        description_string = ""
+        result_class = ""
+        for line in re.split('\n', output.text):
+            description = self._description_regex.match(line)
+            if description:
+                description_string = description.group('description')
+                continue
+
+            result_class_match = self._result_class_regex.match(line)
+            if result_class_match:
+                result_class = result_class_match.group('resultclass')
+                continue
+
+            score = self._score_regex.match(line)
+            if score:
+                key = score.group('key')
+                if key == 'values':
+                    value = [float(number) for number in score.group('value').split(', ')]
+                else:
+                    value = float(score.group('value'))
+                unit = score.group('unit')
+                name = test_name
+                if result_class != 'Time':
+                    name += ':' + result_class.replace(' ', '')
+                if name not in ordered_results_keys:
+                    ordered_results_keys.append(name)
+                results.setdefault(name, {})
+                results[name]['unit'] = unit
+                results[name][key] = value
+                continue
+
+            if not self._should_ignore_line_in_parser_test_result(line):
+                test_failed = True
+                _log.error(line)
+
+        if test_failed:
+            return None
+
+        if set(self._statistics_keys) != set(results[test_name].keys() + ['values']):
+            # values is not provided by Dromaeo tests.
+            _log.error("The test didn't report all statistics.")
+            return None
+
+        for result_name in ordered_results_keys:
+            if result_name == test_name:
+                self.output_statistics(result_name, results[result_name], description_string)
+            else:
+                self.output_statistics(result_name, results[result_name])
+        return results
+
+    def output_statistics(self, test_name, results, description_string=None):
+        unit = results['unit']
+        if description_string:
+            _log.info('DESCRIPTION: %s' % description_string)
+        _log.info('RESULT %s= %s %s' % (test_name.replace(':', ': ').replace('/', ': '), results['avg'], unit))
+        _log.info(', '.join(['%s= %s %s' % (key, results[key], unit) for key in self._statistics_keys[1:5]]))
+
+
+class ChromiumStylePerfTest(PerfTest):
+    _chromium_style_result_regex = re.compile(r'^RESULT\s+(?P<name>[^=]+)\s*=\s+(?P<value>\d+(\.\d+)?)\s*(?P<unit>\w+)$')
+
+    def __init__(self, port, test_name, path_or_url):
+        super(ChromiumStylePerfTest, self).__init__(port, test_name, path_or_url)
+
+    def parse_output(self, output):
+        test_failed = False
+        results = {}
+        for line in re.split('\n', output.text):
+            resultLine = ChromiumStylePerfTest._chromium_style_result_regex.match(line)
+            if resultLine:
+                # FIXME: Store the unit
+                results[self.test_name() + ':' + resultLine.group('name').replace(' ', '')] = float(resultLine.group('value'))
+                _log.info(line)
+            elif not len(line) == 0:
+                test_failed = True
+                _log.error(line)
+        return results if results and not test_failed else None
+
+
+class PageLoadingPerfTest(PerfTest):
+    _FORCE_GC_FILE = 'resources/force-gc.html'
+
+    def __init__(self, port, test_name, path_or_url):
+        super(PageLoadingPerfTest, self).__init__(port, test_name, path_or_url)
+        self.force_gc_test = self._port.host.filesystem.join(self._port.perf_tests_dir(), self._FORCE_GC_FILE)
+
+    def run_single(self, driver, path_or_url, time_out_ms, should_run_pixel_test=False):
+        # Force GC to prevent pageload noise. See https://bugs.webkit.org/show_bug.cgi?id=98203
+        super(PageLoadingPerfTest, self).run_single(driver, self.force_gc_test, time_out_ms, False)
+        return super(PageLoadingPerfTest, self).run_single(driver, path_or_url, time_out_ms, should_run_pixel_test)
+
+    def calculate_statistics(self, values):
+        sorted_values = sorted(values)
+
+        # Compute the mean and variance using Knuth's online algorithm (has good numerical stability).
+        squareSum = 0
+        mean = 0
+        for i, time in enumerate(sorted_values):
+            delta = time - mean
+            sweep = i + 1.0
+            mean += delta / sweep
+            squareSum += delta * (time - mean)
+
+        middle = int(len(sorted_values) / 2)
+        result = {'avg': mean,
+            'min': sorted_values[0],
+            'max': sorted_values[-1],
+            'median': sorted_values[middle] if len(sorted_values) % 2 else (sorted_values[middle - 1] + sorted_values[middle]) / 2,
+            'stdev': math.sqrt(squareSum / (len(sorted_values) - 1))}
+        return result
+
+    def run(self, driver, time_out_ms):
+        results = {}
+        results.setdefault(self.test_name(), {'unit': 'ms', 'values': []})
+
+        for i in range(0, 20):
+            output = self.run_single(driver, self.path_or_url(), time_out_ms)
+            if not output or self.run_failed(output):
+                return None
+            if i == 0:
+                continue
+
+            results[self.test_name()]['values'].append(output.test_time * 1000)
+
+            if not output.measurements:
+                continue
+
+            for result_class, result in output.measurements.items():
+                name = self.test_name() + ':' + result_class
+                if not name in results:
+                    results.setdefault(name, {'values': []})
+                results[name]['values'].append(result)
+                if result_class == 'Malloc' or result_class == 'JSHeap':
+                    results[name]['unit'] = 'bytes'
+
+        for result_class in results.keys():
+            results[result_class].update(self.calculate_statistics(results[result_class]['values']))
+            self.output_statistics(result_class, results[result_class], '')
+
+        return results
+
+
+class ReplayServer(object):
+    def __init__(self, archive, record):
+        self._process = None
+
+        # FIXME: Should error if local proxy isn't set to forward requests to localhost:8080 and localhost:8443
+
+        replay_path = webkitpy.thirdparty.autoinstalled.webpagereplay.replay.__file__
+        args = ['python', replay_path, '--no-dns_forwarding', '--port', '8080', '--ssl_port', '8443', '--use_closest_match', '--log_level', 'warning']
+        if record:
+            args.append('--record')
+        args.append(archive)
+
+        self._process = subprocess.Popen(args)
+
+    def wait_until_ready(self):
+        for i in range(0, 3):
+            try:
+                connection = socket.create_connection(('localhost', '8080'), timeout=1)
+                connection.close()
+                return True
+            except socket.error:
+                time.sleep(1)
+                continue
+        return False
+
+    def stop(self):
+        if self._process:
+            self._process.send_signal(signal.SIGINT)
+            self._process.wait()
+        self._process = None
+
+    def __del__(self):
+        self.stop()
+
+
+class ReplayPerfTest(PageLoadingPerfTest):
+    def __init__(self, port, test_name, path_or_url):
+        super(ReplayPerfTest, self).__init__(port, test_name, path_or_url)
+
+    def _start_replay_server(self, archive, record):
+        try:
+            return ReplayServer(archive, record)
+        except OSError as error:
+            if error.errno == errno.ENOENT:
+                _log.error("Replay tests require web-page-replay.")
+            else:
+                raise error
+
+    def prepare(self, time_out_ms):
+        filesystem = self._port.host.filesystem
+        path_without_ext = filesystem.splitext(self.path_or_url())[0]
+
+        self._archive_path = filesystem.join(path_without_ext + '.wpr')
+        self._expected_image_path = filesystem.join(path_without_ext + '-expected.png')
+        self._url = filesystem.read_text_file(self.path_or_url()).split('\n')[0]
+
+        if filesystem.isfile(self._archive_path) and filesystem.isfile(self._expected_image_path):
+            _log.info("Replay ready for %s" % self._archive_path)
+            return True
+
+        _log.info("Preparing replay for %s" % self.test_name())
+
+        driver = self._port.create_driver(worker_number=1, no_timeout=True)
+        try:
+            output = self.run_single(driver, self._archive_path, time_out_ms, record=True)
+        finally:
+            driver.stop()
+
+        if not output or not filesystem.isfile(self._archive_path):
+            _log.error("Failed to prepare a replay for %s" % self.test_name())
+            return False
+
+        _log.info("Prepared replay for %s" % self.test_name())
+
+        return True
+
+    def run_single(self, driver, url, time_out_ms, record=False):
+        server = self._start_replay_server(self._archive_path, record)
+        if not server:
+            _log.error("Web page replay didn't start.")
+            return None
+
+        try:
+            _log.debug("Waiting for Web page replay to start.")
+            if not server.wait_until_ready():
+                _log.error("Web page replay didn't start.")
+                return None
+
+            _log.debug("Web page replay started. Loading the page.")
+            output = super(ReplayPerfTest, self).run_single(driver, self._url, time_out_ms, should_run_pixel_test=True)
+            if self.run_failed(output):
+                return None
+
+            if not output.image:
+                _log.error("Loading the page did not generate image results")
+                _log.error(output.text)
+                return None
+
+            filesystem = self._port.host.filesystem
+            dirname = filesystem.dirname(self._archive_path)
+            filename = filesystem.split(self._archive_path)[1]
+            writer = TestResultWriter(filesystem, self._port, dirname, filename)
+            if record:
+                writer.write_image_files(actual_image=None, expected_image=output.image)
+            else:
+                writer.write_image_files(actual_image=output.image, expected_image=None)
+
+            return output
+        finally:
+            server.stop()
+
+
+class PerfTestFactory(object):
+
+    _pattern_map = [
+        (re.compile(r'^inspector/'), ChromiumStylePerfTest),
+        (re.compile(r'(.+)\.replay$'), ReplayPerfTest),
+    ]
+
+    @classmethod
+    def create_perf_test(cls, port, test_name, path):
+        for (pattern, test_class) in cls._pattern_map:
+            if pattern.match(test_name):
+                return test_class(port, test_name, path)
+        return PerfTest(port, test_name, path)
diff --git a/Tools/Scripts/webkitpy/performance_tests/perftest_unittest.py b/Tools/Scripts/webkitpy/performance_tests/perftest_unittest.py
new file mode 100755
index 0000000..259fc78
--- /dev/null
+++ b/Tools/Scripts/webkitpy/performance_tests/perftest_unittest.py
@@ -0,0 +1,367 @@
+#!/usr/bin/python
+# Copyright (C) 2012 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 StringIO
+import math
+import unittest
+
+from webkitpy.common.host_mock import MockHost
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.layout_tests.port.driver import DriverOutput
+from webkitpy.layout_tests.port.test import TestDriver
+from webkitpy.layout_tests.port.test import TestPort
+from webkitpy.performance_tests.perftest import ChromiumStylePerfTest
+from webkitpy.performance_tests.perftest import PageLoadingPerfTest
+from webkitpy.performance_tests.perftest import PerfTest
+from webkitpy.performance_tests.perftest import PerfTestFactory
+from webkitpy.performance_tests.perftest import ReplayPerfTest
+
+
+class MockPort(TestPort):
+    def __init__(self, custom_run_test=None):
+        super(MockPort, self).__init__(host=MockHost(), custom_run_test=custom_run_test)
+
+class MainTest(unittest.TestCase):
+    def test_parse_output(self):
+        output = DriverOutput('\n'.join([
+            'Running 20 times',
+            'Ignoring warm-up run (1115)',
+            '',
+            'Time:',
+            'values 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 ms',
+            'avg 1100 ms',
+            'median 1101 ms',
+            'stdev 11 ms',
+            'min 1080 ms',
+            'max 1120 ms']), image=None, image_hash=None, audio=None)
+        output_capture = OutputCapture()
+        output_capture.capture_output()
+        try:
+            test = PerfTest(None, 'some-test', '/path/some-dir/some-test')
+            self.assertEqual(test.parse_output(output),
+                {'some-test': {'avg': 1100.0, 'median': 1101.0, 'min': 1080.0, 'max': 1120.0, 'stdev': 11.0, 'unit': 'ms',
+                    'values': [i for i in range(1, 20)]}})
+        finally:
+            pass
+            actual_stdout, actual_stderr, actual_logs = output_capture.restore_output()
+        self.assertEqual(actual_stdout, '')
+        self.assertEqual(actual_stderr, '')
+        self.assertEqual(actual_logs, 'RESULT some-test= 1100.0 ms\nmedian= 1101.0 ms, stdev= 11.0 ms, min= 1080.0 ms, max= 1120.0 ms\n')
+
+    def test_parse_output_with_failing_line(self):
+        output = DriverOutput('\n'.join([
+            'Running 20 times',
+            'Ignoring warm-up run (1115)',
+            '',
+            'some-unrecognizable-line',
+            '',
+            'Time:'
+            'values 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 ms',
+            'avg 1100 ms',
+            'median 1101 ms',
+            'stdev 11 ms',
+            'min 1080 ms',
+            'max 1120 ms']), image=None, image_hash=None, audio=None)
+        output_capture = OutputCapture()
+        output_capture.capture_output()
+        try:
+            test = PerfTest(None, 'some-test', '/path/some-dir/some-test')
+            self.assertEqual(test.parse_output(output), None)
+        finally:
+            actual_stdout, actual_stderr, actual_logs = output_capture.restore_output()
+        self.assertEqual(actual_stdout, '')
+        self.assertEqual(actual_stderr, '')
+        self.assertEqual(actual_logs, 'some-unrecognizable-line\n')
+
+
+class TestPageLoadingPerfTest(unittest.TestCase):
+    class MockDriver(object):
+        def __init__(self, values, test, measurements=None):
+            self._values = values
+            self._index = 0
+            self._test = test
+            self._measurements = measurements
+
+        def run_test(self, input, stop_when_done):
+            if input.test_name == self._test.force_gc_test:
+                return
+            value = self._values[self._index]
+            self._index += 1
+            if isinstance(value, str):
+                return DriverOutput('some output', image=None, image_hash=None, audio=None, error=value)
+            else:
+                return DriverOutput('some output', image=None, image_hash=None, audio=None, test_time=self._values[self._index - 1], measurements=self._measurements)
+
+    def test_run(self):
+        port = MockPort()
+        test = PageLoadingPerfTest(port, 'some-test', '/path/some-dir/some-test')
+        driver = TestPageLoadingPerfTest.MockDriver(range(1, 21), test)
+        output_capture = OutputCapture()
+        output_capture.capture_output()
+        try:
+            self.assertEqual(test.run(driver, None),
+                {'some-test': {'max': 20000, 'avg': 11000.0, 'median': 11000, 'stdev': 5627.314338711378, 'min': 2000, 'unit': 'ms',
+                    'values': [i * 1000 for i in range(2, 21)]}})
+        finally:
+            actual_stdout, actual_stderr, actual_logs = output_capture.restore_output()
+        self.assertEqual(actual_stdout, '')
+        self.assertEqual(actual_stderr, '')
+        self.assertEqual(actual_logs, 'RESULT some-test= 11000.0 ms\nmedian= 11000 ms, stdev= 5627.31433871 ms, min= 2000 ms, max= 20000 ms\n')
+
+    def test_run_with_memory_output(self):
+        port = MockPort()
+        test = PageLoadingPerfTest(port, 'some-test', '/path/some-dir/some-test')
+        memory_results = {'Malloc': 10, 'JSHeap': 5}
+        self.maxDiff = None
+        driver = TestPageLoadingPerfTest.MockDriver(range(1, 21), test, memory_results)
+        output_capture = OutputCapture()
+        output_capture.capture_output()
+        try:
+            self.assertEqual(test.run(driver, None),
+                {'some-test': {'max': 20000, 'avg': 11000.0, 'median': 11000, 'stdev': 5627.314338711378, 'min': 2000, 'unit': 'ms',
+                    'values': [i * 1000 for i in range(2, 21)]},
+                 'some-test:Malloc': {'max': 10, 'avg': 10.0, 'median': 10, 'min': 10, 'stdev': 0.0, 'unit': 'bytes',
+                    'values': [10] * 19},
+                 'some-test:JSHeap': {'max': 5, 'avg': 5.0, 'median': 5, 'min': 5, 'stdev': 0.0, 'unit': 'bytes',
+                    'values': [5] * 19}})
+        finally:
+            actual_stdout, actual_stderr, actual_logs = output_capture.restore_output()
+        self.assertEqual(actual_stdout, '')
+        self.assertEqual(actual_stderr, '')
+        self.assertEqual(actual_logs, 'RESULT some-test= 11000.0 ms\nmedian= 11000 ms, stdev= 5627.31433871 ms, min= 2000 ms, max= 20000 ms\n'
+            + 'RESULT some-test: Malloc= 10.0 bytes\nmedian= 10 bytes, stdev= 0.0 bytes, min= 10 bytes, max= 10 bytes\n'
+            + 'RESULT some-test: JSHeap= 5.0 bytes\nmedian= 5 bytes, stdev= 0.0 bytes, min= 5 bytes, max= 5 bytes\n')
+
+    def test_run_with_bad_output(self):
+        output_capture = OutputCapture()
+        output_capture.capture_output()
+        try:
+            port = MockPort()
+            test = PageLoadingPerfTest(port, 'some-test', '/path/some-dir/some-test')
+            driver = TestPageLoadingPerfTest.MockDriver([1, 2, 3, 4, 5, 6, 7, 'some error', 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], test)
+            self.assertEqual(test.run(driver, None), None)
+        finally:
+            actual_stdout, actual_stderr, actual_logs = output_capture.restore_output()
+        self.assertEqual(actual_stdout, '')
+        self.assertEqual(actual_stderr, '')
+        self.assertEqual(actual_logs, 'error: some-test\nsome error\n')
+
+
+class TestReplayPerfTest(unittest.TestCase):
+
+    class ReplayTestPort(MockPort):
+        def __init__(self, custom_run_test=None):
+
+            class ReplayTestDriver(TestDriver):
+                def run_test(self, text_input, stop_when_done):
+                    return custom_run_test(text_input, stop_when_done) if custom_run_test else None
+
+            self._custom_driver_class = ReplayTestDriver
+            super(self.__class__, self).__init__()
+
+        def _driver_class(self):
+            return self._custom_driver_class
+
+    class MockReplayServer(object):
+        def __init__(self, wait_until_ready=True):
+            self.wait_until_ready = lambda: wait_until_ready
+
+        def stop(self):
+            pass
+
+    def _add_file(self, port, dirname, filename, content=True):
+        port.host.filesystem.maybe_make_directory(dirname)
+        port.host.filesystem.write_binary_file(port.host.filesystem.join(dirname, filename), content)
+
+    def _setup_test(self, run_test=None):
+        test_port = self.ReplayTestPort(run_test)
+        self._add_file(test_port, '/path/some-dir', 'some-test.replay', 'http://some-test/')
+        test = ReplayPerfTest(test_port, 'some-test.replay', '/path/some-dir/some-test.replay')
+        test._start_replay_server = lambda archive, record: self.__class__.MockReplayServer()
+        return test, test_port
+
+    def test_run_single(self):
+        output_capture = OutputCapture()
+        output_capture.capture_output()
+
+        loaded_pages = []
+
+        def run_test(test_input, stop_when_done):
+            if test_input.test_name == test.force_gc_test:
+                loaded_pages.append(test_input)
+                return
+            if test_input.test_name != "about:blank":
+                self.assertEqual(test_input.test_name, 'http://some-test/')
+            loaded_pages.append(test_input)
+            self._add_file(port, '/path/some-dir', 'some-test.wpr', 'wpr content')
+            return DriverOutput('actual text', 'actual image', 'actual checksum',
+                audio=None, crash=False, timeout=False, error=False)
+
+        test, port = self._setup_test(run_test)
+        test._archive_path = '/path/some-dir/some-test.wpr'
+        test._url = 'http://some-test/'
+
+        try:
+            driver = port.create_driver(worker_number=1, no_timeout=True)
+            self.assertTrue(test.run_single(driver, '/path/some-dir/some-test.replay', time_out_ms=100))
+        finally:
+            actual_stdout, actual_stderr, actual_logs = output_capture.restore_output()
+
+        self.assertEqual(len(loaded_pages), 2)
+        self.assertEqual(loaded_pages[0].test_name, test.force_gc_test)
+        self.assertEqual(loaded_pages[1].test_name, 'http://some-test/')
+        self.assertEqual(actual_stdout, '')
+        self.assertEqual(actual_stderr, '')
+        self.assertEqual(actual_logs, '')
+        self.assertEqual(port.host.filesystem.read_binary_file('/path/some-dir/some-test-actual.png'), 'actual image')
+
+    def test_run_single_fails_without_webpagereplay(self):
+        output_capture = OutputCapture()
+        output_capture.capture_output()
+
+        test, port = self._setup_test()
+        test._start_replay_server = lambda archive, record: None
+        test._archive_path = '/path/some-dir.wpr'
+        test._url = 'http://some-test/'
+
+        try:
+            driver = port.create_driver(worker_number=1, no_timeout=True)
+            self.assertEqual(test.run_single(driver, '/path/some-dir/some-test.replay', time_out_ms=100), None)
+        finally:
+            actual_stdout, actual_stderr, actual_logs = output_capture.restore_output()
+        self.assertEqual(actual_stdout, '')
+        self.assertEqual(actual_stderr, '')
+        self.assertEqual(actual_logs, "Web page replay didn't start.\n")
+
+    def test_prepare_fails_when_wait_until_ready_fails(self):
+        output_capture = OutputCapture()
+        output_capture.capture_output()
+
+        test, port = self._setup_test()
+        test._start_replay_server = lambda archive, record: self.__class__.MockReplayServer(wait_until_ready=False)
+        test._archive_path = '/path/some-dir.wpr'
+        test._url = 'http://some-test/'
+
+        try:
+            driver = port.create_driver(worker_number=1, no_timeout=True)
+            self.assertEqual(test.run_single(driver, '/path/some-dir/some-test.replay', time_out_ms=100), None)
+        finally:
+            actual_stdout, actual_stderr, actual_logs = output_capture.restore_output()
+
+        self.assertEqual(actual_stdout, '')
+        self.assertEqual(actual_stderr, '')
+        self.assertEqual(actual_logs, "Web page replay didn't start.\n")
+
+    def test_run_single_fails_when_output_has_error(self):
+        output_capture = OutputCapture()
+        output_capture.capture_output()
+
+        loaded_pages = []
+
+        def run_test(test_input, stop_when_done):
+            loaded_pages.append(test_input)
+            self._add_file(port, '/path/some-dir', 'some-test.wpr', 'wpr content')
+            return DriverOutput('actual text', 'actual image', 'actual checksum',
+                audio=None, crash=False, timeout=False, error='some error')
+
+        test, port = self._setup_test(run_test)
+        test._archive_path = '/path/some-dir.wpr'
+        test._url = 'http://some-test/'
+
+        try:
+            driver = port.create_driver(worker_number=1, no_timeout=True)
+            self.assertEqual(test.run_single(driver, '/path/some-dir/some-test.replay', time_out_ms=100), None)
+        finally:
+            actual_stdout, actual_stderr, actual_logs = output_capture.restore_output()
+
+        self.assertEqual(len(loaded_pages), 2)
+        self.assertEqual(loaded_pages[0].test_name, test.force_gc_test)
+        self.assertEqual(loaded_pages[1].test_name, 'http://some-test/')
+        self.assertEqual(actual_stdout, '')
+        self.assertEqual(actual_stderr, '')
+        self.assertEqual(actual_logs, 'error: some-test.replay\nsome error\n')
+
+    def test_prepare(self):
+        output_capture = OutputCapture()
+        output_capture.capture_output()
+
+        def run_test(test_input, stop_when_done):
+            self._add_file(port, '/path/some-dir', 'some-test.wpr', 'wpr content')
+            return DriverOutput('actual text', 'actual image', 'actual checksum',
+                audio=None, crash=False, timeout=False, error=False)
+
+        test, port = self._setup_test(run_test)
+
+        try:
+            self.assertEqual(test.prepare(time_out_ms=100), True)
+        finally:
+            actual_stdout, actual_stderr, actual_logs = output_capture.restore_output()
+
+        self.assertEqual(actual_stdout, '')
+        self.assertEqual(actual_stderr, '')
+        self.assertEqual(actual_logs, 'Preparing replay for some-test.replay\nPrepared replay for some-test.replay\n')
+        self.assertEqual(port.host.filesystem.read_binary_file('/path/some-dir/some-test-expected.png'), 'actual image')
+
+    def test_prepare_calls_run_single(self):
+        output_capture = OutputCapture()
+        output_capture.capture_output()
+        called = [False]
+
+        def run_single(driver, url, time_out_ms, record):
+            self.assertTrue(record)
+            self.assertEqual(url, '/path/some-dir/some-test.wpr')
+            called[0] = True
+            return False
+
+        test, port = self._setup_test()
+        test.run_single = run_single
+
+        try:
+            self.assertEqual(test.prepare(time_out_ms=100), False)
+        finally:
+            actual_stdout, actual_stderr, actual_logs = output_capture.restore_output()
+        self.assertTrue(called[0])
+        self.assertEqual(test._archive_path, '/path/some-dir/some-test.wpr')
+        self.assertEqual(test._url, 'http://some-test/')
+        self.assertEqual(actual_stdout, '')
+        self.assertEqual(actual_stderr, '')
+        self.assertEqual(actual_logs, "Preparing replay for some-test.replay\nFailed to prepare a replay for some-test.replay\n")
+
+class TestPerfTestFactory(unittest.TestCase):
+    def test_regular_test(self):
+        test = PerfTestFactory.create_perf_test(MockPort(), 'some-dir/some-test', '/path/some-dir/some-test')
+        self.assertEqual(test.__class__, PerfTest)
+
+    def test_inspector_test(self):
+        test = PerfTestFactory.create_perf_test(MockPort(), 'inspector/some-test', '/path/inspector/some-test')
+        self.assertEqual(test.__class__, ChromiumStylePerfTest)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/performance_tests/perftestsrunner.py b/Tools/Scripts/webkitpy/performance_tests/perftestsrunner.py
new file mode 100755
index 0000000..42e0d96
--- /dev/null
+++ b/Tools/Scripts/webkitpy/performance_tests/perftestsrunner.py
@@ -0,0 +1,324 @@
+#!/usr/bin/env python
+# Copyright (C) 2012 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.
+
+"""Run Inspector's perf tests in perf mode."""
+
+import os
+import json
+import logging
+import optparse
+import time
+
+from webkitpy.common import find_files
+from webkitpy.common.checkout.scm.detection import SCMDetector
+from webkitpy.common.host import Host
+from webkitpy.common.net.file_uploader import FileUploader
+from webkitpy.performance_tests.perftest import PerfTestFactory
+
+
+_log = logging.getLogger(__name__)
+
+
+class PerfTestsRunner(object):
+    _default_branch = 'webkit-trunk'
+    EXIT_CODE_BAD_BUILD = -1
+    EXIT_CODE_BAD_SOURCE_JSON = -2
+    EXIT_CODE_BAD_MERGE = -3
+    EXIT_CODE_FAILED_UPLOADING = -4
+    EXIT_CODE_BAD_PREPARATION = -5
+
+    _DEFAULT_JSON_FILENAME = 'PerformanceTestsResults.json'
+
+    def __init__(self, args=None, port=None):
+        self._options, self._args = PerfTestsRunner._parse_args(args)
+        if port:
+            self._port = port
+            self._host = self._port.host
+        else:
+            self._host = Host()
+            self._port = self._host.port_factory.get(self._options.platform, self._options)
+        self._host.initialize_scm()
+        self._webkit_base_dir_len = len(self._port.webkit_base())
+        self._base_path = self._port.perf_tests_dir()
+        self._results = {}
+        self._timestamp = time.time()
+
+    @staticmethod
+    def _parse_args(args=None):
+        def _expand_path(option, opt_str, value, parser):
+            path = os.path.expandvars(os.path.expanduser(value))
+            setattr(parser.values, option.dest, path)
+        perf_option_list = [
+            optparse.make_option('--debug', action='store_const', const='Debug', dest="configuration",
+                help='Set the configuration to Debug'),
+            optparse.make_option('--release', action='store_const', const='Release', dest="configuration",
+                help='Set the configuration to Release'),
+            optparse.make_option("--platform",
+                help="Specify port/platform being tested (i.e. chromium-mac)"),
+            optparse.make_option("--chromium",
+                action="store_const", const='chromium', dest='platform', help='Alias for --platform=chromium'),
+            optparse.make_option("--builder-name",
+                help=("The name of the builder shown on the waterfall running this script e.g. google-mac-2.")),
+            optparse.make_option("--build-number",
+                help=("The build number of the builder running this script.")),
+            optparse.make_option("--build", dest="build", action="store_true", default=True,
+                help="Check to ensure the DumpRenderTree build is up-to-date (default)."),
+            optparse.make_option("--no-build", dest="build", action="store_false",
+                help="Don't check to see if the DumpRenderTree build is up-to-date."),
+            optparse.make_option("--build-directory",
+                help="Path to the directory under which build files are kept (should not include configuration)"),
+            optparse.make_option("--time-out-ms", default=600 * 1000,
+                help="Set the timeout for each test"),
+            optparse.make_option("--pause-before-testing", dest="pause_before_testing", action="store_true", default=False,
+                help="Pause before running the tests to let user attach a performance monitor."),
+            optparse.make_option("--no-results", action="store_false", dest="generate_results", default=True,
+                help="Do no generate results JSON and results page."),
+            optparse.make_option("--output-json-path", action='callback', callback=_expand_path, type="str",
+                help="Path to generate a JSON file at; may contain previous results if it already exists."),
+            optparse.make_option("--reset-results", action="store_true",
+                help="Clears the content in the generated JSON file before adding the results."),
+            optparse.make_option("--slave-config-json-path", action='callback', callback=_expand_path, type="str",
+                help="Only used on bots. Path to a slave configuration file."),
+            optparse.make_option("--description",
+                help="Add a description to the output JSON file if one is generated"),
+            optparse.make_option("--no-show-results", action="store_false", default=True, dest="show_results",
+                help="Don't launch a browser with results after the tests are done"),
+            optparse.make_option("--test-results-server",
+                help="Upload the generated JSON file to the specified server when --output-json-path is present."),
+            optparse.make_option("--webkit-test-runner", "-2", action="store_true",
+                help="Use WebKitTestRunner rather than DumpRenderTree."),
+            optparse.make_option("--replay", dest="replay", action="store_true", default=False,
+                help="Run replay tests."),
+            optparse.make_option("--force", dest="skipped", action="store_true", default=False,
+                help="Run all tests, including the ones in the Skipped list."),
+            ]
+        return optparse.OptionParser(option_list=(perf_option_list)).parse_args(args)
+
+    def _collect_tests(self):
+        """Return the list of tests found."""
+
+        test_extensions = ['.html', '.svg']
+        if self._options.replay:
+            test_extensions.append('.replay')
+
+        def _is_test_file(filesystem, dirname, filename):
+            return filesystem.splitext(filename)[1] in test_extensions
+
+        filesystem = self._host.filesystem
+
+        paths = []
+        for arg in self._args:
+            paths.append(arg)
+            relpath = filesystem.relpath(arg, self._base_path)
+            if relpath:
+                paths.append(relpath)
+
+        skipped_directories = set(['.svn', 'resources'])
+        test_files = find_files.find(filesystem, self._base_path, paths, skipped_directories, _is_test_file)
+        tests = []
+        for path in test_files:
+            relative_path = self._port.relative_perf_test_filename(path).replace('\\', '/')
+            if self._port.skips_perf_test(relative_path) and not self._options.skipped:
+                continue
+            test = PerfTestFactory.create_perf_test(self._port, relative_path, path)
+            tests.append(test)
+
+        return tests
+
+    def run(self):
+        if not self._port.check_build(needs_http=False):
+            _log.error("Build not up to date for %s" % self._port._path_to_driver())
+            return self.EXIT_CODE_BAD_BUILD
+
+        tests = self._collect_tests()
+        _log.info("Running %d tests" % len(tests))
+
+        for test in tests:
+            if not test.prepare(self._options.time_out_ms):
+                return self.EXIT_CODE_BAD_PREPARATION
+
+        unexpected = self._run_tests_set(sorted(list(tests), key=lambda test: test.test_name()), self._port)
+        if self._options.generate_results:
+            exit_code = self._generate_and_show_results()
+            if exit_code:
+                return exit_code
+
+        return unexpected
+
+    def _output_json_path(self):
+        output_json_path = self._options.output_json_path
+        if output_json_path:
+            return output_json_path
+        return self._host.filesystem.join(self._port.perf_results_directory(), self._DEFAULT_JSON_FILENAME)
+
+    def _generate_and_show_results(self):
+        options = self._options
+        output_json_path = self._output_json_path()
+        output = self._generate_results_dict(self._timestamp, options.description, options.platform, options.builder_name, options.build_number)
+
+        if options.slave_config_json_path:
+            output = self._merge_slave_config_json(options.slave_config_json_path, output)
+            if not output:
+                return self.EXIT_CODE_BAD_SOURCE_JSON
+
+        output = self._merge_outputs_if_needed(output_json_path, output)
+        if not output:
+            return self.EXIT_CODE_BAD_MERGE
+
+        results_page_path = self._host.filesystem.splitext(output_json_path)[0] + '.html'
+        self._generate_output_files(output_json_path, results_page_path, output)
+
+        if options.test_results_server:
+            if not self._upload_json(options.test_results_server, output_json_path):
+                return self.EXIT_CODE_FAILED_UPLOADING
+
+        if options.show_results:
+            self._port.show_results_html_file(results_page_path)
+
+    def _generate_results_dict(self, timestamp, description, platform, builder_name, build_number):
+        contents = {'results': self._results}
+        if description:
+            contents['description'] = description
+        for (name, path) in self._port.repository_paths():
+            scm = SCMDetector(self._host.filesystem, self._host.executive).detect_scm_system(path) or self._host.scm()
+            contents[name + '-revision'] = scm.svn_revision(path)
+
+        # FIXME: Add --branch or auto-detect the branch we're in
+        for key, value in {'timestamp': int(timestamp), 'branch': self._default_branch, 'platform': platform,
+            'builder-name': builder_name, 'build-number': int(build_number) if build_number else None}.items():
+            if value:
+                contents[key] = value
+
+        return contents
+
+    def _merge_slave_config_json(self, slave_config_json_path, output):
+        if not self._host.filesystem.isfile(slave_config_json_path):
+            _log.error("Missing slave configuration JSON file: %s" % slave_config_json_path)
+            return None
+
+        try:
+            slave_config_json = self._host.filesystem.open_text_file_for_reading(slave_config_json_path)
+            slave_config = json.load(slave_config_json)
+            return dict(slave_config.items() + output.items())
+        except Exception, error:
+            _log.error("Failed to merge slave configuration JSON file %s: %s" % (slave_config_json_path, error))
+        return None
+
+    def _merge_outputs_if_needed(self, output_json_path, output):
+        if self._options.reset_results or not self._host.filesystem.isfile(output_json_path):
+            return [output]
+        try:
+            existing_outputs = json.loads(self._host.filesystem.read_text_file(output_json_path))
+            return existing_outputs + [output]
+        except Exception, error:
+            _log.error("Failed to merge output JSON file %s: %s" % (output_json_path, error))
+        return None
+
+    def _generate_output_files(self, output_json_path, results_page_path, output):
+        filesystem = self._host.filesystem
+
+        json_output = json.dumps(output)
+        filesystem.write_text_file(output_json_path, json_output)
+
+        if results_page_path:
+            template_path = filesystem.join(self._port.perf_tests_dir(), 'resources/results-template.html')
+            template = filesystem.read_text_file(template_path)
+
+            absolute_path_to_trunk = filesystem.dirname(self._port.perf_tests_dir())
+            results_page = template.replace('%AbsolutePathToWebKitTrunk%', absolute_path_to_trunk)
+            results_page = results_page.replace('%PeformanceTestsResultsJSON%', json_output)
+
+            filesystem.write_text_file(results_page_path, results_page)
+
+    def _upload_json(self, test_results_server, json_path, file_uploader=FileUploader):
+        uploader = file_uploader("https://%s/api/test/report" % test_results_server, 120)
+        try:
+            response = uploader.upload_single_text_file(self._host.filesystem, 'application/json', json_path)
+        except Exception, error:
+            _log.error("Failed to upload JSON file in 120s: %s" % error)
+            return False
+
+        response_body = [line.strip('\n') for line in response]
+        if response_body != ['OK']:
+            _log.error("Uploaded JSON but got a bad response:")
+            for line in response_body:
+                _log.error(line)
+            return False
+
+        _log.info("JSON file uploaded.")
+        return True
+
+    def _print_status(self, tests, expected, unexpected):
+        if len(tests) == expected + unexpected:
+            status = "Ran %d tests" % len(tests)
+        else:
+            status = "Running %d of %d tests" % (expected + unexpected + 1, len(tests))
+        if unexpected:
+            status += " (%d didn't run)" % unexpected
+        _log.info(status)
+
+    def _run_tests_set(self, tests, port):
+        result_count = len(tests)
+        expected = 0
+        unexpected = 0
+        driver = None
+
+        for test in tests:
+            driver = port.create_driver(worker_number=1, no_timeout=True)
+
+            if self._options.pause_before_testing:
+                driver.start()
+                if not self._host.user.confirm("Ready to run test?"):
+                    driver.stop()
+                    return unexpected
+
+            _log.info('Running %s (%d of %d)' % (test.test_name(), expected + unexpected + 1, len(tests)))
+            if self._run_single_test(test, driver):
+                expected = expected + 1
+            else:
+                unexpected = unexpected + 1
+
+            _log.info('')
+
+            driver.stop()
+
+        return unexpected
+
+    def _run_single_test(self, test, driver):
+        start_time = time.time()
+
+        new_results = test.run(driver, self._options.time_out_ms)
+        if new_results:
+            self._results.update(new_results)
+        else:
+            _log.error('FAILED')
+
+        _log.info("Finished: %f s" % (time.time() - start_time))
+
+        return new_results != None
diff --git a/Tools/Scripts/webkitpy/performance_tests/perftestsrunner_unittest.py b/Tools/Scripts/webkitpy/performance_tests/perftestsrunner_unittest.py
new file mode 100755
index 0000000..9c9295f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/performance_tests/perftestsrunner_unittest.py
@@ -0,0 +1,642 @@
+#!/usr/bin/python
+# Copyright (C) 2012 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.
+
+"""Unit tests for run_perf_tests."""
+
+import StringIO
+import json
+import re
+import unittest
+
+from webkitpy.common.host_mock import MockHost
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.layout_tests.port.driver import DriverInput, DriverOutput
+from webkitpy.layout_tests.port.test import TestPort
+from webkitpy.layout_tests.views import printing
+from webkitpy.performance_tests.perftest import ChromiumStylePerfTest
+from webkitpy.performance_tests.perftest import PerfTest
+from webkitpy.performance_tests.perftestsrunner import PerfTestsRunner
+
+
+class MainTest(unittest.TestCase):
+    def assertWritten(self, stream, contents):
+        self.assertEquals(stream.buflist, contents)
+
+    def normalizeFinishedTime(self, log):
+        return re.sub(r'Finished: [0-9\.]+ s', 'Finished: 0.1 s', log)
+
+    class TestDriver:
+        def run_test(self, driver_input, stop_when_done):
+            text = ''
+            timeout = False
+            crash = False
+            if driver_input.test_name.endswith('pass.html'):
+                text = 'RESULT group_name: test_name= 42 ms'
+            elif driver_input.test_name.endswith('timeout.html'):
+                timeout = True
+            elif driver_input.test_name.endswith('failed.html'):
+                text = None
+            elif driver_input.test_name.endswith('tonguey.html'):
+                text = 'we are not expecting an output from perf tests but RESULT blablabla'
+            elif driver_input.test_name.endswith('crash.html'):
+                crash = True
+            elif driver_input.test_name.endswith('event-target-wrapper.html'):
+                text = """Running 20 times
+Ignoring warm-up run (1502)
+1504
+1505
+1510
+1504
+1507
+1509
+1510
+1487
+1488
+1472
+1472
+1488
+1473
+1472
+1475
+1487
+1486
+1486
+1475
+1471
+
+Time:
+values 1504, 1505, 1510, 1504, 1507, 1509, 1510, 1487, 1488, 1472, 1472, 1488, 1473, 1472, 1475, 1487, 1486, 1486, 1475, 1471 ms
+avg 1489.05 ms
+median 1487 ms
+stdev 14.46 ms
+min 1471 ms
+max 1510 ms
+"""
+            elif driver_input.test_name.endswith('some-parser.html'):
+                text = """Running 20 times
+Ignoring warm-up run (1115)
+
+Time:
+values 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 ms
+avg 1100 ms
+median 1101 ms
+stdev 11 ms
+min 1080 ms
+max 1120 ms
+"""
+            elif driver_input.test_name.endswith('memory-test.html'):
+                text = """Running 20 times
+Ignoring warm-up run (1115)
+
+Time:
+values 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 ms
+avg 1100 ms
+median 1101 ms
+stdev 11 ms
+min 1080 ms
+max 1120 ms
+
+JS Heap:
+values 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 bytes
+avg 832000 bytes
+median 829000 bytes
+stdev 15000 bytes
+min 811000 bytes
+max 848000 bytes
+
+Malloc:
+values 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 bytes
+avg 532000 bytes
+median 529000 bytes
+stdev 13000 bytes
+min 511000 bytes
+max 548000 bytes
+"""
+            return DriverOutput(text, '', '', '', crash=crash, timeout=timeout)
+
+        def start(self):
+            """do nothing"""
+
+        def stop(self):
+            """do nothing"""
+
+    def create_runner(self, args=[], driver_class=TestDriver):
+        options, parsed_args = PerfTestsRunner._parse_args(args)
+        test_port = TestPort(host=MockHost(), options=options)
+        test_port.create_driver = lambda worker_number=None, no_timeout=False: driver_class()
+
+        runner = PerfTestsRunner(args=args, port=test_port)
+        runner._host.filesystem.maybe_make_directory(runner._base_path, 'inspector')
+        runner._host.filesystem.maybe_make_directory(runner._base_path, 'Bindings')
+        runner._host.filesystem.maybe_make_directory(runner._base_path, 'Parser')
+
+        filesystem = runner._host.filesystem
+        runner.load_output_json = lambda: json.loads(filesystem.read_text_file(runner._output_json_path()))
+        return runner, test_port
+
+    def run_test(self, test_name):
+        runner, port = self.create_runner()
+        driver = MainTest.TestDriver()
+        return runner._run_single_test(ChromiumStylePerfTest(port, test_name, runner._host.filesystem.join('some-dir', test_name)), driver)
+
+    def test_run_passing_test(self):
+        self.assertTrue(self.run_test('pass.html'))
+
+    def test_run_silent_test(self):
+        self.assertFalse(self.run_test('silent.html'))
+
+    def test_run_failed_test(self):
+        self.assertFalse(self.run_test('failed.html'))
+
+    def test_run_tonguey_test(self):
+        self.assertFalse(self.run_test('tonguey.html'))
+
+    def test_run_timeout_test(self):
+        self.assertFalse(self.run_test('timeout.html'))
+
+    def test_run_crash_test(self):
+        self.assertFalse(self.run_test('crash.html'))
+
+    def _tests_for_runner(self, runner, test_names):
+        filesystem = runner._host.filesystem
+        tests = []
+        for test in test_names:
+            path = filesystem.join(runner._base_path, test)
+            dirname = filesystem.dirname(path)
+            if test.startswith('inspector/'):
+                tests.append(ChromiumStylePerfTest(runner._port, test, path))
+            else:
+                tests.append(PerfTest(runner._port, test, path))
+        return tests
+
+    def test_run_test_set(self):
+        runner, port = self.create_runner()
+        tests = self._tests_for_runner(runner, ['inspector/pass.html', 'inspector/silent.html', 'inspector/failed.html',
+            'inspector/tonguey.html', 'inspector/timeout.html', 'inspector/crash.html'])
+        output = OutputCapture()
+        output.capture_output()
+        try:
+            unexpected_result_count = runner._run_tests_set(tests, port)
+        finally:
+            stdout, stderr, log = output.restore_output()
+        self.assertEqual(unexpected_result_count, len(tests) - 1)
+        self.assertTrue('\nRESULT group_name: test_name= 42 ms\n' in log)
+
+    def test_run_test_set_kills_drt_per_run(self):
+
+        class TestDriverWithStopCount(MainTest.TestDriver):
+            stop_count = 0
+
+            def stop(self):
+                TestDriverWithStopCount.stop_count += 1
+
+        runner, port = self.create_runner(driver_class=TestDriverWithStopCount)
+
+        tests = self._tests_for_runner(runner, ['inspector/pass.html', 'inspector/silent.html', 'inspector/failed.html',
+            'inspector/tonguey.html', 'inspector/timeout.html', 'inspector/crash.html'])
+        unexpected_result_count = runner._run_tests_set(tests, port)
+
+        self.assertEqual(TestDriverWithStopCount.stop_count, 6)
+
+    def test_run_test_pause_before_testing(self):
+        class TestDriverWithStartCount(MainTest.TestDriver):
+            start_count = 0
+
+            def start(self):
+                TestDriverWithStartCount.start_count += 1
+
+        runner, port = self.create_runner(args=["--pause-before-testing"], driver_class=TestDriverWithStartCount)
+        tests = self._tests_for_runner(runner, ['inspector/pass.html'])
+
+        output = OutputCapture()
+        output.capture_output()
+        try:
+            unexpected_result_count = runner._run_tests_set(tests, port)
+            self.assertEqual(TestDriverWithStartCount.start_count, 1)
+        finally:
+            stdout, stderr, log = output.restore_output()
+        self.assertEqual(stderr, "Ready to run test?\n")
+        self.assertEqual(self.normalizeFinishedTime(log),
+            "Running inspector/pass.html (1 of 1)\nRESULT group_name: test_name= 42 ms\nFinished: 0.1 s\n\n")
+
+    def test_run_test_set_for_parser_tests(self):
+        runner, port = self.create_runner()
+        tests = self._tests_for_runner(runner, ['Bindings/event-target-wrapper.html', 'Parser/some-parser.html'])
+        output = OutputCapture()
+        output.capture_output()
+        try:
+            unexpected_result_count = runner._run_tests_set(tests, port)
+        finally:
+            stdout, stderr, log = output.restore_output()
+        self.assertEqual(unexpected_result_count, 0)
+        self.assertEqual(self.normalizeFinishedTime(log), '\n'.join(['Running Bindings/event-target-wrapper.html (1 of 2)',
+        'RESULT Bindings: event-target-wrapper= 1489.05 ms',
+        'median= 1487.0 ms, stdev= 14.46 ms, min= 1471.0 ms, max= 1510.0 ms',
+        'Finished: 0.1 s',
+        '',
+        'Running Parser/some-parser.html (2 of 2)',
+        'RESULT Parser: some-parser= 1100.0 ms',
+        'median= 1101.0 ms, stdev= 11.0 ms, min= 1080.0 ms, max= 1120.0 ms',
+        'Finished: 0.1 s',
+        '', '']))
+
+    def test_run_memory_test(self):
+        runner, port = self.create_runner_and_setup_results_template()
+        runner._timestamp = 123456789
+        port.host.filesystem.write_text_file(runner._base_path + '/Parser/memory-test.html', 'some content')
+
+        output = OutputCapture()
+        output.capture_output()
+        try:
+            unexpected_result_count = runner.run()
+        finally:
+            stdout, stderr, log = output.restore_output()
+        self.assertEqual(unexpected_result_count, 0)
+        self.assertEqual(self.normalizeFinishedTime(log), '\n'.join([
+            'Running 1 tests',
+            'Running Parser/memory-test.html (1 of 1)',
+            'RESULT Parser: memory-test= 1100.0 ms',
+            'median= 1101.0 ms, stdev= 11.0 ms, min= 1080.0 ms, max= 1120.0 ms',
+            'RESULT Parser: memory-test: JSHeap= 832000.0 bytes',
+            'median= 829000.0 bytes, stdev= 15000.0 bytes, min= 811000.0 bytes, max= 848000.0 bytes',
+            'RESULT Parser: memory-test: Malloc= 532000.0 bytes',
+            'median= 529000.0 bytes, stdev= 13000.0 bytes, min= 511000.0 bytes, max= 548000.0 bytes',
+            'Finished: 0.1 s',
+            '', '']))
+        results = runner.load_output_json()[0]['results']
+        values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
+        self.assertEqual(results['Parser/memory-test'], {'min': 1080.0, 'max': 1120.0, 'median': 1101.0, 'stdev': 11.0, 'avg': 1100.0, 'unit': 'ms', 'values': values})
+        self.assertEqual(results['Parser/memory-test:JSHeap'], {'min': 811000.0, 'max': 848000.0, 'median': 829000.0, 'stdev': 15000.0, 'avg': 832000.0, 'unit': 'bytes', 'values': values})
+        self.assertEqual(results['Parser/memory-test:Malloc'], {'min': 511000.0, 'max': 548000.0, 'median': 529000.0, 'stdev': 13000.0, 'avg': 532000.0, 'unit': 'bytes', 'values': values})
+
+    def _test_run_with_json_output(self, runner, filesystem, upload_suceeds=False, expected_exit_code=0):
+        filesystem.write_text_file(runner._base_path + '/inspector/pass.html', 'some content')
+        filesystem.write_text_file(runner._base_path + '/Bindings/event-target-wrapper.html', 'some content')
+
+        uploaded = [False]
+
+        def mock_upload_json(hostname, json_path):
+            self.assertEqual(hostname, 'some.host')
+            self.assertEqual(json_path, '/mock-checkout/output.json')
+            uploaded[0] = upload_suceeds
+            return upload_suceeds
+
+        runner._upload_json = mock_upload_json
+        runner._timestamp = 123456789
+        output_capture = OutputCapture()
+        output_capture.capture_output()
+        try:
+            self.assertEqual(runner.run(), expected_exit_code)
+        finally:
+            stdout, stderr, logs = output_capture.restore_output()
+
+        if not expected_exit_code:
+            self.assertEqual(self.normalizeFinishedTime(logs),
+                '\n'.join(['Running 2 tests',
+                'Running Bindings/event-target-wrapper.html (1 of 2)',
+                'RESULT Bindings: event-target-wrapper= 1489.05 ms',
+                'median= 1487.0 ms, stdev= 14.46 ms, min= 1471.0 ms, max= 1510.0 ms',
+                'Finished: 0.1 s',
+                '',
+                'Running inspector/pass.html (2 of 2)',
+                'RESULT group_name: test_name= 42 ms',
+                'Finished: 0.1 s',
+                '',
+                '']))
+
+        self.assertEqual(uploaded[0], upload_suceeds)
+
+        return logs
+
+    _event_target_wrapper_and_inspector_results = {
+        "Bindings/event-target-wrapper": {"max": 1510, "avg": 1489.05, "median": 1487, "min": 1471, "stdev": 14.46, "unit": "ms",
+           "values": [1504, 1505, 1510, 1504, 1507, 1509, 1510, 1487, 1488, 1472, 1472, 1488, 1473, 1472, 1475, 1487, 1486, 1486, 1475, 1471]},
+        "inspector/pass.html:group_name:test_name": 42}
+
+    def test_run_with_json_output(self):
+        runner, port = self.create_runner_and_setup_results_template(args=['--output-json-path=/mock-checkout/output.json',
+            '--test-results-server=some.host'])
+        self._test_run_with_json_output(runner, port.host.filesystem, upload_suceeds=True)
+        self.assertEqual(runner.load_output_json(), [{
+            "timestamp": 123456789, "results": self._event_target_wrapper_and_inspector_results,
+            "webkit-revision": "5678", "branch": "webkit-trunk"}])
+
+        filesystem = port.host.filesystem
+        self.assertTrue(filesystem.isfile(runner._output_json_path()))
+        self.assertTrue(filesystem.isfile(filesystem.splitext(runner._output_json_path())[0] + '.html'))
+
+    def test_run_with_description(self):
+        runner, port = self.create_runner_and_setup_results_template(args=['--output-json-path=/mock-checkout/output.json',
+            '--test-results-server=some.host', '--description', 'some description'])
+        self._test_run_with_json_output(runner, port.host.filesystem, upload_suceeds=True)
+        self.assertEqual(runner.load_output_json(), [{
+            "timestamp": 123456789, "description": "some description",
+            "results": self._event_target_wrapper_and_inspector_results,
+            "webkit-revision": "5678", "branch": "webkit-trunk"}])
+
+    def create_runner_and_setup_results_template(self, args=[]):
+        runner, port = self.create_runner(args)
+        filesystem = port.host.filesystem
+        filesystem.write_text_file(runner._base_path + '/resources/results-template.html',
+            'BEGIN<script src="%AbsolutePathToWebKitTrunk%/some.js"></script>'
+            '<script src="%AbsolutePathToWebKitTrunk%/other.js"></script><script>%PeformanceTestsResultsJSON%</script>END')
+        filesystem.write_text_file(runner._base_path + '/Dromaeo/resources/dromaeo/web/lib/jquery-1.6.4.js', 'jquery content')
+        return runner, port
+
+    def test_run_respects_no_results(self):
+        runner, port = self.create_runner(args=['--output-json-path=/mock-checkout/output.json',
+            '--test-results-server=some.host', '--no-results'])
+        self._test_run_with_json_output(runner, port.host.filesystem, upload_suceeds=False)
+        self.assertFalse(port.host.filesystem.isfile('/mock-checkout/output.json'))
+
+    def test_run_generates_json_by_default(self):
+        runner, port = self.create_runner_and_setup_results_template()
+        filesystem = port.host.filesystem
+        output_json_path = runner._output_json_path()
+        results_page_path = filesystem.splitext(output_json_path)[0] + '.html'
+
+        self.assertFalse(filesystem.isfile(output_json_path))
+        self.assertFalse(filesystem.isfile(results_page_path))
+
+        self._test_run_with_json_output(runner, port.host.filesystem)
+
+        self.assertEqual(json.loads(port.host.filesystem.read_text_file(output_json_path)), [{
+            "timestamp": 123456789, "results": self._event_target_wrapper_and_inspector_results,
+            "webkit-revision": "5678", "branch": "webkit-trunk"}])
+
+        self.assertTrue(filesystem.isfile(output_json_path))
+        self.assertTrue(filesystem.isfile(results_page_path))
+
+    def test_run_merges_output_by_default(self):
+        runner, port = self.create_runner_and_setup_results_template()
+        filesystem = port.host.filesystem
+        output_json_path = runner._output_json_path()
+
+        filesystem.write_text_file(output_json_path, '[{"previous": "results"}]')
+
+        self._test_run_with_json_output(runner, port.host.filesystem)
+
+        self.assertEqual(json.loads(port.host.filesystem.read_text_file(output_json_path)), [{"previous": "results"}, {
+            "timestamp": 123456789, "results": self._event_target_wrapper_and_inspector_results,
+            "webkit-revision": "5678", "branch": "webkit-trunk"}])
+        self.assertTrue(filesystem.isfile(filesystem.splitext(output_json_path)[0] + '.html'))
+
+    def test_run_respects_reset_results(self):
+        runner, port = self.create_runner_and_setup_results_template(args=["--reset-results"])
+        filesystem = port.host.filesystem
+        output_json_path = runner._output_json_path()
+
+        filesystem.write_text_file(output_json_path, '[{"previous": "results"}]')
+
+        self._test_run_with_json_output(runner, port.host.filesystem)
+
+        self.assertEqual(json.loads(port.host.filesystem.read_text_file(output_json_path)), [{
+            "timestamp": 123456789, "results": self._event_target_wrapper_and_inspector_results,
+            "webkit-revision": "5678", "branch": "webkit-trunk"}])
+        self.assertTrue(filesystem.isfile(filesystem.splitext(output_json_path)[0] + '.html'))
+        pass
+
+    def test_run_generates_and_show_results_page(self):
+        runner, port = self.create_runner_and_setup_results_template(args=['--output-json-path=/mock-checkout/output.json'])
+        page_shown = []
+        port.show_results_html_file = lambda path: page_shown.append(path)
+        filesystem = port.host.filesystem
+        self._test_run_with_json_output(runner, filesystem)
+
+        expected_entry = {"timestamp": 123456789, "results": self._event_target_wrapper_and_inspector_results,
+            "webkit-revision": "5678", "branch": "webkit-trunk"}
+
+        self.maxDiff = None
+        json_output = port.host.filesystem.read_text_file('/mock-checkout/output.json')
+        self.assertEqual(json.loads(json_output), [expected_entry])
+        self.assertEqual(filesystem.read_text_file('/mock-checkout/output.html'),
+            'BEGIN<script src="/test.checkout/some.js"></script><script src="/test.checkout/other.js"></script>'
+            '<script>%s</script>END' % json_output)
+        self.assertEqual(page_shown[0], '/mock-checkout/output.html')
+
+        self._test_run_with_json_output(runner, filesystem)
+        json_output = port.host.filesystem.read_text_file('/mock-checkout/output.json')
+        self.assertEqual(json.loads(json_output), [expected_entry, expected_entry])
+        self.assertEqual(filesystem.read_text_file('/mock-checkout/output.html'),
+            'BEGIN<script src="/test.checkout/some.js"></script><script src="/test.checkout/other.js"></script>'
+            '<script>%s</script>END' % json_output)
+
+    def test_run_respects_no_show_results(self):
+        show_results_html_file = lambda path: page_shown.append(path)
+
+        runner, port = self.create_runner_and_setup_results_template(args=['--output-json-path=/mock-checkout/output.json'])
+        page_shown = []
+        port.show_results_html_file = show_results_html_file
+        self._test_run_with_json_output(runner, port.host.filesystem)
+        self.assertEqual(page_shown[0], '/mock-checkout/output.html')
+
+        runner, port = self.create_runner_and_setup_results_template(args=['--output-json-path=/mock-checkout/output.json',
+            '--no-show-results'])
+        page_shown = []
+        port.show_results_html_file = show_results_html_file
+        self._test_run_with_json_output(runner, port.host.filesystem)
+        self.assertEqual(page_shown, [])
+
+    def test_run_with_bad_output_json(self):
+        runner, port = self.create_runner_and_setup_results_template(args=['--output-json-path=/mock-checkout/output.json'])
+        port.host.filesystem.write_text_file('/mock-checkout/output.json', 'bad json')
+        self._test_run_with_json_output(runner, port.host.filesystem, expected_exit_code=PerfTestsRunner.EXIT_CODE_BAD_MERGE)
+        port.host.filesystem.write_text_file('/mock-checkout/output.json', '{"another bad json": "1"}')
+        self._test_run_with_json_output(runner, port.host.filesystem, expected_exit_code=PerfTestsRunner.EXIT_CODE_BAD_MERGE)
+
+    def test_run_with_slave_config_json(self):
+        runner, port = self.create_runner_and_setup_results_template(args=['--output-json-path=/mock-checkout/output.json',
+            '--slave-config-json-path=/mock-checkout/slave-config.json', '--test-results-server=some.host'])
+        port.host.filesystem.write_text_file('/mock-checkout/slave-config.json', '{"key": "value"}')
+        self._test_run_with_json_output(runner, port.host.filesystem, upload_suceeds=True)
+        self.assertEqual(runner.load_output_json(), [{
+            "timestamp": 123456789, "results": self._event_target_wrapper_and_inspector_results,
+            "webkit-revision": "5678", "branch": "webkit-trunk", "key": "value"}])
+
+    def test_run_with_bad_slave_config_json(self):
+        runner, port = self.create_runner_and_setup_results_template(args=['--output-json-path=/mock-checkout/output.json',
+            '--slave-config-json-path=/mock-checkout/slave-config.json', '--test-results-server=some.host'])
+        logs = self._test_run_with_json_output(runner, port.host.filesystem, expected_exit_code=PerfTestsRunner.EXIT_CODE_BAD_SOURCE_JSON)
+        self.assertTrue('Missing slave configuration JSON file: /mock-checkout/slave-config.json' in logs)
+        port.host.filesystem.write_text_file('/mock-checkout/slave-config.json', 'bad json')
+        self._test_run_with_json_output(runner, port.host.filesystem, expected_exit_code=PerfTestsRunner.EXIT_CODE_BAD_SOURCE_JSON)
+        port.host.filesystem.write_text_file('/mock-checkout/slave-config.json', '["another bad json"]')
+        self._test_run_with_json_output(runner, port.host.filesystem, expected_exit_code=PerfTestsRunner.EXIT_CODE_BAD_SOURCE_JSON)
+
+    def test_run_with_multiple_repositories(self):
+        runner, port = self.create_runner_and_setup_results_template(args=['--output-json-path=/mock-checkout/output.json',
+            '--test-results-server=some.host'])
+        port.repository_paths = lambda: [('webkit', '/mock-checkout'), ('some', '/mock-checkout/some')]
+        self._test_run_with_json_output(runner, port.host.filesystem, upload_suceeds=True)
+        self.assertEqual(runner.load_output_json(), [{
+            "timestamp": 123456789, "results": self._event_target_wrapper_and_inspector_results,
+            "webkit-revision": "5678", "some-revision": "5678", "branch": "webkit-trunk"}])
+
+    def test_run_with_upload_json(self):
+        runner, port = self.create_runner_and_setup_results_template(args=['--output-json-path=/mock-checkout/output.json',
+            '--test-results-server', 'some.host', '--platform', 'platform1', '--builder-name', 'builder1', '--build-number', '123'])
+
+        self._test_run_with_json_output(runner, port.host.filesystem, upload_suceeds=True)
+        generated_json = json.loads(port.host.filesystem.files['/mock-checkout/output.json'])
+        self.assertEqual(generated_json[0]['platform'], 'platform1')
+        self.assertEqual(generated_json[0]['builder-name'], 'builder1')
+        self.assertEqual(generated_json[0]['build-number'], 123)
+
+        self._test_run_with_json_output(runner, port.host.filesystem, upload_suceeds=False, expected_exit_code=PerfTestsRunner.EXIT_CODE_FAILED_UPLOADING)
+
+    def test_upload_json(self):
+        runner, port = self.create_runner()
+        port.host.filesystem.files['/mock-checkout/some.json'] = 'some content'
+
+        called = []
+        upload_single_text_file_throws = False
+        upload_single_text_file_return_value = StringIO.StringIO('OK')
+
+        class MockFileUploader:
+            def __init__(mock, url, timeout):
+                self.assertEqual(url, 'https://some.host/api/test/report')
+                self.assertTrue(isinstance(timeout, int) and timeout)
+                called.append('FileUploader')
+
+            def upload_single_text_file(mock, filesystem, content_type, filename):
+                self.assertEqual(filesystem, port.host.filesystem)
+                self.assertEqual(content_type, 'application/json')
+                self.assertEqual(filename, 'some.json')
+                called.append('upload_single_text_file')
+                if upload_single_text_file_throws:
+                    raise "Some exception"
+                return upload_single_text_file_return_value
+
+        runner._upload_json('some.host', 'some.json', MockFileUploader)
+        self.assertEqual(called, ['FileUploader', 'upload_single_text_file'])
+
+        output = OutputCapture()
+        output.capture_output()
+        upload_single_text_file_return_value = StringIO.StringIO('Some error')
+        runner._upload_json('some.host', 'some.json', MockFileUploader)
+        _, _, logs = output.restore_output()
+        self.assertEqual(logs, 'Uploaded JSON but got a bad response:\nSome error\n')
+
+        # Throwing an exception upload_single_text_file shouldn't blow up _upload_json
+        called = []
+        upload_single_text_file_throws = True
+        runner._upload_json('some.host', 'some.json', MockFileUploader)
+        self.assertEqual(called, ['FileUploader', 'upload_single_text_file'])
+
+    def _add_file(self, runner, dirname, filename, content=True):
+        dirname = runner._host.filesystem.join(runner._base_path, dirname) if dirname else runner._base_path
+        runner._host.filesystem.maybe_make_directory(dirname)
+        runner._host.filesystem.files[runner._host.filesystem.join(dirname, filename)] = content
+
+    def test_collect_tests(self):
+        runner, port = self.create_runner()
+        self._add_file(runner, 'inspector', 'a_file.html', 'a content')
+        tests = runner._collect_tests()
+        self.assertEqual(len(tests), 1)
+
+    def _collect_tests_and_sort_test_name(self, runner):
+        return sorted([test.test_name() for test in runner._collect_tests()])
+
+    def test_collect_tests_with_multile_files(self):
+        runner, port = self.create_runner(args=['PerformanceTests/test1.html', 'test2.html'])
+
+        def add_file(filename):
+            port.host.filesystem.files[runner._host.filesystem.join(runner._base_path, filename)] = 'some content'
+
+        add_file('test1.html')
+        add_file('test2.html')
+        add_file('test3.html')
+        port.host.filesystem.chdir(runner._port.perf_tests_dir()[:runner._port.perf_tests_dir().rfind(runner._host.filesystem.sep)])
+        self.assertEqual(self._collect_tests_and_sort_test_name(runner), ['test1.html', 'test2.html'])
+
+    def test_collect_tests_with_skipped_list(self):
+        runner, port = self.create_runner()
+
+        self._add_file(runner, 'inspector', 'test1.html')
+        self._add_file(runner, 'inspector', 'unsupported_test1.html')
+        self._add_file(runner, 'inspector', 'test2.html')
+        self._add_file(runner, 'inspector/resources', 'resource_file.html')
+        self._add_file(runner, 'unsupported', 'unsupported_test2.html')
+        port.skipped_perf_tests = lambda: ['inspector/unsupported_test1.html', 'unsupported']
+        self.assertEqual(self._collect_tests_and_sort_test_name(runner), ['inspector/test1.html', 'inspector/test2.html'])
+
+    def test_collect_tests_with_skipped_list(self):
+        runner, port = self.create_runner(args=['--force'])
+
+        self._add_file(runner, 'inspector', 'test1.html')
+        self._add_file(runner, 'inspector', 'unsupported_test1.html')
+        self._add_file(runner, 'inspector', 'test2.html')
+        self._add_file(runner, 'inspector/resources', 'resource_file.html')
+        self._add_file(runner, 'unsupported', 'unsupported_test2.html')
+        port.skipped_perf_tests = lambda: ['inspector/unsupported_test1.html', 'unsupported']
+        self.assertEqual(self._collect_tests_and_sort_test_name(runner), ['inspector/test1.html', 'inspector/test2.html', 'inspector/unsupported_test1.html', 'unsupported/unsupported_test2.html'])
+
+    def test_collect_tests_should_ignore_replay_tests_by_default(self):
+        runner, port = self.create_runner()
+        self._add_file(runner, 'Replay', 'www.webkit.org.replay')
+        self.assertEqual(runner._collect_tests(), [])
+
+    def test_collect_tests_with_replay_tests(self):
+        runner, port = self.create_runner(args=['--replay'])
+        self._add_file(runner, 'Replay', 'www.webkit.org.replay')
+        tests = runner._collect_tests()
+        self.assertEqual(len(tests), 1)
+        self.assertEqual(tests[0].__class__.__name__, 'ReplayPerfTest')
+
+    def test_parse_args(self):
+        runner, port = self.create_runner()
+        options, args = PerfTestsRunner._parse_args([
+                '--build-directory=folder42',
+                '--platform=platform42',
+                '--builder-name', 'webkit-mac-1',
+                '--build-number=56',
+                '--time-out-ms=42',
+                '--no-show-results',
+                '--reset-results',
+                '--output-json-path=a/output.json',
+                '--slave-config-json-path=a/source.json',
+                '--test-results-server=somehost',
+                '--debug'])
+        self.assertEqual(options.build, True)
+        self.assertEqual(options.build_directory, 'folder42')
+        self.assertEqual(options.platform, 'platform42')
+        self.assertEqual(options.builder_name, 'webkit-mac-1')
+        self.assertEqual(options.build_number, '56')
+        self.assertEqual(options.time_out_ms, '42')
+        self.assertEqual(options.configuration, 'Debug')
+        self.assertEqual(options.show_results, False)
+        self.assertEqual(options.reset_results, True)
+        self.assertEqual(options.output_json_path, 'a/output.json')
+        self.assertEqual(options.slave_config_json_path, 'a/source.json')
+        self.assertEqual(options.test_results_server, 'somehost')
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/pylintrc b/Tools/Scripts/webkitpy/pylintrc
new file mode 100644
index 0000000..caadcfb
--- /dev/null
+++ b/Tools/Scripts/webkitpy/pylintrc
@@ -0,0 +1,312 @@
+# Copyright (c) 2012 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.
+
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Profiled execution.
+profile=no
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+
+[MESSAGES CONTROL]
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time.
+#enable=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once).
+# CHANGED:
+# C0103: Invalid name ""
+# C0111: Missing docstring
+# C0301: Line too long
+# C0302: Too many lines in module (N)
+# I0010: Unable to consider inline option ''
+# I0011: Locally disabling WNNNN
+#
+# R0201: Method could be a function
+# R0801: Similar lines in N files
+# R0901: Too many ancestors (8/7)
+# R0902: Too many instance attributes (N/7)
+# R0903: Too few public methods (N/2)
+# R0904: Too many public methods (N/20)
+# R0911: Too many return statements (N/6)
+# R0912: Too many branches (N/12)
+# R0913: Too many arguments (N/5)
+# R0914: Too many local variables (N/15)
+# R0915: Too many statements (N/50)
+# R0921: Abstract class not referenced
+# R0922: Abstract class is only referenced 1 times
+# W0122: Use of the exec statement
+# W0141: Used builtin function ''
+# W0212: Access to a protected member X of a client class
+# W0142: Used * or ** magic
+# W0401: Wildcard import X
+# W0402: Uses of a deprecated module 'string'
+# W0404: 41: Reimport 'XX' (imported line NN)
+# W0511: TODO
+# W0603: Using the global statement
+# W0614: Unused import X from wildcard import
+# W0703: Catch "Exception"
+# W1201: Specify string format arguments as logging function parameters
+disable=C0103,C0111,C0301,C0302,I0010,I0011,R0201,R0801,R0901,R0902,R0903,R0904,R0911,R0912,R0913,R0914,R0915,R0921,R0922,W0122,W0141,W0142,W0212,W0401,W0402,W0404,W0511,W0603,W0614,W0703,W1201
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html
+output-format=text
+
+# Include message's id in output
+include-ids=yes
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells whether to display a full report or only the messages
+# CHANGED:
+reports=no
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Add a comment according to your evaluation note. This is used by the global
+# evaluation report (RP0004).
+comment=no
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the beginning of the name of dummy variables
+# (i.e. not used).
+dummy-variables-rgx=_|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+
+[TYPECHECK]
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamically set).
+ignored-classes=SQLObject,twisted.internet.reactor,hashlib,google.appengine.api.memcache
+
+# When zope mode is activated, add a predefined set of Zope acquired attributes
+# to generated-members.
+zope=no
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E0201 when accessed. Python regular
+# expressions are accepted.
+generated-members=REQUEST,acl_users,aq_parent
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=200
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+# CHANGED:
+indent-string='    '
+
+
+[BASIC]
+
+# Required attributes for module, separated by a comma
+required-attributes=
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=map,filter,apply,input
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Regular expression which should only match correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression which should only match correct function names
+function-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct method names
+method-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct instance attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,ex,Run,_
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Regular expression which should only match functions or classes name which do
+# not require a docstring
+no-docstring-rgx=__.*__
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+max-branchs=12
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+
+[CLASSES]
+
+# List of interface methods to ignore, separated by a comma. This is used for
+# instance to not check methods defines in Zope's Interface base class.
+ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception
diff --git a/Tools/Scripts/webkitpy/style/__init__.py b/Tools/Scripts/webkitpy/style/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/style/checker.py b/Tools/Scripts/webkitpy/style/checker.py
new file mode 100644
index 0000000..9f27c36
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checker.py
@@ -0,0 +1,860 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+# Copyright (C) 2010 ProFUSION embedded systems
+#
+# 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.
+
+"""Front end of some style-checker modules."""
+
+import logging
+import os.path
+import re
+import sys
+
+from checkers.common import categories as CommonCategories
+from checkers.common import CarriageReturnChecker
+from checkers.changelog import ChangeLogChecker
+from checkers.cpp import CppChecker
+from checkers.jsonchecker import JSONChecker
+from checkers.png import PNGChecker
+from checkers.python import PythonChecker
+from checkers.test_expectations import TestExpectationsChecker
+from checkers.text import TextChecker
+from checkers.watchlist import WatchListChecker
+from checkers.xcodeproj import XcodeProjectFileChecker
+from checkers.xml import XMLChecker
+from error_handlers import DefaultStyleErrorHandler
+from filter import FilterConfiguration
+from optparser import ArgumentParser
+from optparser import DefaultCommandOptionValues
+from webkitpy.common.system.logutils import configure_logging as _configure_logging
+
+
+_log = logging.getLogger(__name__)
+
+
+# These are default option values for the command-line option parser.
+_DEFAULT_MIN_CONFIDENCE = 1
+_DEFAULT_OUTPUT_FORMAT = 'emacs'
+
+
+# FIXME: For style categories we will never want to have, remove them.
+#        For categories for which we want to have similar functionality,
+#        modify the implementation and enable them.
+#
+# Throughout this module, we use "filter rule" rather than "filter"
+# for an individual boolean filter flag like "+foo".  This allows us to
+# reserve "filter" for what one gets by collectively applying all of
+# the filter rules.
+#
+# The base filter rules are the filter rules that begin the list of
+# filter rules used to check style.  For example, these rules precede
+# any user-specified filter rules.  Since by default all categories are
+# checked, this list should normally include only rules that begin
+# with a "-" sign.
+_BASE_FILTER_RULES = [
+    '-build/endif_comment',
+    '-build/include_what_you_use',  # <string> for std::string
+    '-build/storage_class',  # const static
+    '-legal/copyright',
+    '-readability/multiline_comment',
+    '-readability/braces',  # int foo() {};
+    '-readability/fn_size',
+    '-readability/casting',
+    '-readability/function',
+    '-runtime/arrays',  # variable length array
+    '-runtime/casting',
+    '-runtime/sizeof',
+    '-runtime/explicit',  # explicit
+    '-runtime/virtual',  # virtual dtor
+    '-runtime/printf',
+    '-runtime/threadsafe_fn',
+    '-runtime/rtti',
+    '-whitespace/blank_line',
+    '-whitespace/end_of_line',
+    # List Python pep8 categories last.
+    #
+    # Because much of WebKit's Python code base does not abide by the
+    # PEP8 79 character limit, we ignore the 79-character-limit category
+    # pep8/E501 for now.
+    #
+    # FIXME: Consider bringing WebKit's Python code base into conformance
+    #        with the 79 character limit, or some higher limit that is
+    #        agreeable to the WebKit project.
+    '-pep8/E501',
+    ]
+
+
+# The path-specific filter rules.
+#
+# This list is order sensitive.  Only the first path substring match
+# is used.  See the FilterConfiguration documentation in filter.py
+# for more information on this list.
+#
+# Each string appearing in this nested list should have at least
+# one associated unit test assertion.  These assertions are located,
+# for example, in the test_path_rules_specifier() unit test method of
+# checker_unittest.py.
+_PATH_RULES_SPECIFIER = [
+    # Files in these directories are consumers of the WebKit
+    # API and therefore do not follow the same header including
+    # discipline as WebCore.
+
+    ([# TestNetscapePlugIn has no config.h and uses funny names like
+      # NPP_SetWindow.
+      "Tools/DumpRenderTree/TestNetscapePlugIn/",
+      # The API test harnesses have no config.h and use funny macros like
+      # TEST_CLASS_NAME.
+      "Tools/WebKitAPITest/",
+      "Tools/TestWebKitAPI/",
+      "Source/WebKit/qt/tests/qdeclarativewebview"],
+     ["-build/include",
+      "-readability/naming"]),
+    ([# There is no clean way to avoid "yy_*" names used by flex.
+      "Source/WebCore/css/CSSParser.cpp",
+      # Qt code uses '_' in some places (such as private slots
+      # and on test xxx_data methos on tests)
+      "Source/JavaScriptCore/qt/",
+      "Source/WebKit/qt/tests/",
+      "Source/WebKit/qt/declarative/",
+      "Source/WebKit/qt/examples/"],
+     ["-readability/naming"]),
+
+    ([# The Qt APIs use Qt declaration style, it puts the * to
+      # the variable name, not to the class.
+      "Source/WebKit/qt/Api/"],
+     ["-readability/naming",
+      "-whitespace/declaration"]),
+
+     ([# Qt's MiniBrowser has no config.h
+       "Tools/MiniBrowser/qt",
+       "Tools/MiniBrowser/qt/raw"],
+      ["-build/include"]),
+
+    ([# The Qt APIs use Qt/QML naming style, which includes
+      # naming parameters in h files.
+      "Source/WebKit2/UIProcess/API/qt"],
+     ["-readability/parameter_name"]),
+
+    ([# The GTK+ APIs use GTK+ naming style, which includes
+      # lower-cased, underscore-separated values, whitespace before
+      # parens for function calls, and always having variable names.
+      # Also, GTK+ allows the use of NULL.
+      "Source/WebCore/bindings/scripts/test/GObject",
+      "Source/WebKit/gtk/webkit/",
+      "Tools/DumpRenderTree/gtk/"],
+     ["-readability/naming",
+      "-readability/parameter_name",
+      "-readability/null",
+      "-whitespace/parens"]),
+    ([# Header files in ForwardingHeaders have no header guards or
+      # 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.
+      "Source/JavaScriptCore/assembler/",
+      "Source/JavaScriptCore/jit/JIT"],
+     ["-readability/naming/underscores"]),
+    ([# JITStubs has an usual syntax which causes false alarms for a few checks.
+      "JavaScriptCore/jit/JITStubs.cpp"],
+     ["-readability/parameter_name",
+      "-whitespace/parens"]),
+
+    ([# The EFL APIs use EFL naming style, which includes
+      # both lower-cased and camel-cased, underscore-sparated
+      # values.
+      "Source/WebKit/efl/ewk/",
+      "Source/WebKit2/UIProcess/API/efl/"],
+     ["-readability/naming",
+      "-readability/parameter_name"]),
+    ([# EWebLauncher and MiniBrowser are EFL simple application.
+      # They need to use efl coding style and they don't have config.h.
+      "Tools/EWebLauncher/",
+      "Tools/MiniBrowser/efl/"],
+     ["-readability/naming",
+      "-readability/parameter_name",
+      "-whitespace/declaration",
+      "-build/include_order"]),
+
+    # WebKit2 rules:
+    # WebKit2 and certain directories have idiosyncracies.
+    ([# NPAPI has function names with underscores.
+      "Source/WebKit2/WebProcess/Plugins/Netscape"],
+     ["-readability/naming"]),
+    ([# The WebKit2 C API has names with underscores and whitespace-aligned
+      # struct members. Also, we allow unnecessary parameter names in
+      # WebKit2 APIs because we're matching CF's header style.
+      "Source/WebKit2/UIProcess/API/C/",
+      "Source/WebKit2/Shared/API/c/",
+      "Source/WebKit2/WebProcess/InjectedBundle/API/c/"],
+     ["-readability/naming",
+      "-readability/parameter_name",
+      "-whitespace/declaration"]),
+    ([# These files define GObjects, which implies some definitions of
+      # variables and functions containing underscores.
+      "Source/WebCore/platform/graphics/gstreamer/VideoSinkGStreamer1.cpp",
+      "Source/WebCore/platform/graphics/gstreamer/VideoSinkGStreamer.cpp",
+      "Source/WebCore/platform/graphics/gstreamer/WebKitWebSourceGStreamer.cpp",
+      "Source/WebCore/platform/audio/gstreamer/WebKitWebAudioSourceGStreamer.cpp",
+      "Source/WebCore/platform/network/soup/ProxyResolverSoup.cpp",
+      "Source/WebCore/platform/network/soup/ProxyResolverSoup.h"],
+     ["-readability/naming"]),
+
+    # For third-party Python code, keep only the following checks--
+    #
+    #   No tabs: to avoid having to set the SVN allow-tabs property.
+    #   No trailing white space: since this is easy to correct.
+    #   No carriage-return line endings: since this is easy to correct.
+    #
+    (["webkitpy/thirdparty/"],
+     ["-",
+      "+pep8/W191",  # Tabs
+      "+pep8/W291",  # Trailing white space
+      "+whitespace/carriage_return"]),
+
+    ([# glu's libtess is third-party code, and doesn't follow WebKit style.
+      "Source/ThirdParty/glu"],
+     ["-readability",
+      "-whitespace",
+      "-build/header_guard",
+      "-build/include_order"]),
+
+    ([# There is no way to avoid the symbols __jit_debug_register_code
+      # and __jit_debug_descriptor when integrating with gdb.
+      "Source/JavaScriptCore/jit/GDBInterface.cpp"],
+     ["-readability/naming"]),
+
+    ([# On some systems the trailing CR is causing parser failure.
+      "Source/JavaScriptCore/parser/Keywords.table"],
+     ["+whitespace/carriage_return"]),
+]
+
+
+_CPP_FILE_EXTENSIONS = [
+    'c',
+    'cpp',
+    'h',
+    ]
+
+_JSON_FILE_EXTENSION = 'json'
+
+_PYTHON_FILE_EXTENSION = 'py'
+
+_TEXT_FILE_EXTENSIONS = [
+    'ac',
+    'cc',
+    'cgi',
+    'css',
+    'exp',
+    'flex',
+    'gyp',
+    'gypi',
+    'html',
+    'idl',
+    'in',
+    'js',
+    'mm',
+    'php',
+    'pl',
+    'pm',
+    'pri',
+    'pro',
+    'rb',
+    'sh',
+    'table',
+    'txt',
+    'wm',
+    'xhtml',
+    'y',
+    ]
+
+_XCODEPROJ_FILE_EXTENSION = 'pbxproj'
+
+_XML_FILE_EXTENSIONS = [
+    'vcproj',
+    'vsprops',
+    ]
+
+_PNG_FILE_EXTENSION = 'png'
+
+# Files to skip that are less obvious.
+#
+# Some files should be skipped when checking style. For example,
+# WebKit maintains some files in Mozilla style on purpose to ease
+# future merges.
+_SKIPPED_FILES_WITH_WARNING = [
+    "Source/WebKit/gtk/tests/",
+    # All WebKit*.h files in Source/WebKit2/UIProcess/API/gtk,
+    # except those ending in ...Private.h are GTK+ API headers,
+    # which differ greatly from WebKit coding style.
+    re.compile(r'Source/WebKit2/UIProcess/API/gtk/WebKit(?!.*Private\.h).*\.h$'),
+    'Source/WebKit2/UIProcess/API/gtk/webkit2.h']
+
+# Files to skip that are more common or obvious.
+#
+# This list should be in addition to files with FileType.NONE.  Files
+# with FileType.NONE are automatically skipped without warning.
+_SKIPPED_FILES_WITHOUT_WARNING = [
+    "LayoutTests" + os.path.sep,
+    ]
+
+# Extensions of files which are allowed to contain carriage returns.
+_CARRIAGE_RETURN_ALLOWED_FILE_EXTENSIONS = [
+    'png',
+    'vcproj',
+    'vsprops',
+    ]
+
+# The maximum number of errors to report per file, per category.
+# If a category is not a key, then it has no maximum.
+_MAX_REPORTS_PER_CATEGORY = {
+    "whitespace/carriage_return": 1
+}
+
+
+def _all_categories():
+    """Return the set of all categories used by check-webkit-style."""
+    # Take the union across all checkers.
+    categories = CommonCategories.union(CppChecker.categories)
+    categories = categories.union(JSONChecker.categories)
+    categories = categories.union(TestExpectationsChecker.categories)
+    categories = categories.union(ChangeLogChecker.categories)
+    categories = categories.union(PNGChecker.categories)
+
+    # FIXME: Consider adding all of the pep8 categories.  Since they
+    #        are not too meaningful for documentation purposes, for
+    #        now we add only the categories needed for the unit tests
+    #        (which validate the consistency of the configuration
+    #        settings against the known categories, etc).
+    categories = categories.union(["pep8/W191", "pep8/W291", "pep8/E501"])
+
+    return categories
+
+
+def _check_webkit_style_defaults():
+    """Return the default command-line options for check-webkit-style."""
+    return DefaultCommandOptionValues(min_confidence=_DEFAULT_MIN_CONFIDENCE,
+                                      output_format=_DEFAULT_OUTPUT_FORMAT)
+
+
+# This function assists in optparser not having to import from checker.
+def check_webkit_style_parser():
+    all_categories = _all_categories()
+    default_options = _check_webkit_style_defaults()
+    return ArgumentParser(all_categories=all_categories,
+                          base_filter_rules=_BASE_FILTER_RULES,
+                          default_options=default_options)
+
+
+def check_webkit_style_configuration(options):
+    """Return a StyleProcessorConfiguration instance for check-webkit-style.
+
+    Args:
+      options: A CommandOptionValues instance.
+
+    """
+    filter_configuration = FilterConfiguration(
+                               base_rules=_BASE_FILTER_RULES,
+                               path_specific=_PATH_RULES_SPECIFIER,
+                               user_rules=options.filter_rules)
+
+    return StyleProcessorConfiguration(filter_configuration=filter_configuration,
+               max_reports_per_category=_MAX_REPORTS_PER_CATEGORY,
+               min_confidence=options.min_confidence,
+               output_format=options.output_format,
+               stderr_write=sys.stderr.write)
+
+
+def _create_log_handlers(stream):
+    """Create and return a default list of logging.Handler instances.
+
+    Format WARNING messages and above to display the logging level, and
+    messages strictly below WARNING not to display it.
+
+    Args:
+      stream: See the configure_logging() docstring.
+
+    """
+    # Handles logging.WARNING and above.
+    error_handler = logging.StreamHandler(stream)
+    error_handler.setLevel(logging.WARNING)
+    formatter = logging.Formatter("%(levelname)s: %(message)s")
+    error_handler.setFormatter(formatter)
+
+    # Create a logging.Filter instance that only accepts messages
+    # below WARNING (i.e. filters out anything WARNING or above).
+    non_error_filter = logging.Filter()
+    # The filter method accepts a logging.LogRecord instance.
+    non_error_filter.filter = lambda record: record.levelno < logging.WARNING
+
+    non_error_handler = logging.StreamHandler(stream)
+    non_error_handler.addFilter(non_error_filter)
+    formatter = logging.Formatter("%(message)s")
+    non_error_handler.setFormatter(formatter)
+
+    return [error_handler, non_error_handler]
+
+
+def _create_debug_log_handlers(stream):
+    """Create and return a list of logging.Handler instances for debugging.
+
+    Args:
+      stream: See the configure_logging() docstring.
+
+    """
+    handler = logging.StreamHandler(stream)
+    formatter = logging.Formatter("%(name)s: %(levelname)-8s %(message)s")
+    handler.setFormatter(formatter)
+
+    return [handler]
+
+
+def configure_logging(stream, logger=None, is_verbose=False):
+    """Configure logging, and return the list of handlers added.
+
+    Returns:
+      A list of references to the logging handlers added to the root
+      logger.  This allows the caller to later remove the handlers
+      using logger.removeHandler.  This is useful primarily during unit
+      testing where the caller may want to configure logging temporarily
+      and then undo the configuring.
+
+    Args:
+      stream: A file-like object to which to log.  The stream must
+              define an "encoding" data attribute, or else logging
+              raises an error.
+      logger: A logging.logger instance to configure.  This parameter
+              should be used only in unit tests.  Defaults to the
+              root logger.
+      is_verbose: A boolean value of whether logging should be verbose.
+
+    """
+    # If the stream does not define an "encoding" data attribute, the
+    # logging module can throw an error like the following:
+    #
+    # Traceback (most recent call last):
+    #   File "/System/Library/Frameworks/Python.framework/Versions/2.6/...
+    #         lib/python2.6/logging/__init__.py", line 761, in emit
+    #     self.stream.write(fs % msg.encode(self.stream.encoding))
+    # LookupError: unknown encoding: unknown
+    if logger is None:
+        logger = logging.getLogger()
+
+    if is_verbose:
+        logging_level = logging.DEBUG
+        handlers = _create_debug_log_handlers(stream)
+    else:
+        logging_level = logging.INFO
+        handlers = _create_log_handlers(stream)
+
+    handlers = _configure_logging(logging_level=logging_level, logger=logger,
+                                  handlers=handlers)
+
+    return handlers
+
+
+# Enum-like idiom
+class FileType:
+
+    NONE = 0  # FileType.NONE evaluates to False.
+    # Alphabetize remaining types
+    CHANGELOG = 1
+    CPP = 2
+    JSON = 3
+    PNG = 4
+    PYTHON = 5
+    TEXT = 6
+    WATCHLIST = 7
+    XML = 8
+    XCODEPROJ = 9
+
+
+class CheckerDispatcher(object):
+
+    """Supports determining whether and how to check style, based on path."""
+
+    def _file_extension(self, file_path):
+        """Return the file extension without the leading dot."""
+        return os.path.splitext(file_path)[1].lstrip(".")
+
+    def _should_skip_file_path(self, file_path, skip_array_entry):
+        match = re.search("\s*png$", file_path)
+        if match:
+            return False
+        if isinstance(skip_array_entry, str):
+            if file_path.find(skip_array_entry) >= 0:
+                return True
+        elif skip_array_entry.match(file_path):
+                return True
+        return False
+
+    def should_skip_with_warning(self, file_path):
+        """Return whether the given file should be skipped with a warning."""
+        for skipped_file in _SKIPPED_FILES_WITH_WARNING:
+            if self._should_skip_file_path(file_path, skipped_file):
+                return True
+        return False
+
+    def should_skip_without_warning(self, file_path):
+        """Return whether the given file should be skipped without a warning."""
+        if not self._file_type(file_path):  # FileType.NONE.
+            return True
+        # Since "LayoutTests" is in _SKIPPED_FILES_WITHOUT_WARNING, make
+        # an exception to prevent files like "LayoutTests/ChangeLog" and
+        # "LayoutTests/ChangeLog-2009-06-16" from being skipped.
+        # Files like 'TestExpectations' are also should not be skipped.
+        #
+        # FIXME: Figure out a good way to avoid having to add special logic
+        #        for this special case.
+        basename = os.path.basename(file_path)
+        if basename.startswith('ChangeLog'):
+            return False
+        elif basename == 'TestExpectations':
+            return False
+        for skipped_file in _SKIPPED_FILES_WITHOUT_WARNING:
+            if self._should_skip_file_path(file_path, skipped_file):
+                return True
+        return False
+
+    def should_check_and_strip_carriage_returns(self, file_path):
+        return self._file_extension(file_path) not in _CARRIAGE_RETURN_ALLOWED_FILE_EXTENSIONS
+
+    def _file_type(self, file_path):
+        """Return the file type corresponding to the given file."""
+        file_extension = self._file_extension(file_path)
+
+        if (file_extension in _CPP_FILE_EXTENSIONS) or (file_path == '-'):
+            # FIXME: Do something about the comment below and the issue it
+            #        raises since cpp_style already relies on the extension.
+            #
+            # Treat stdin as C++. Since the extension is unknown when
+            # reading from stdin, cpp_style tests should not rely on
+            # the extension.
+            return FileType.CPP
+        elif file_extension == _JSON_FILE_EXTENSION:
+            return FileType.JSON
+        elif file_extension == _PYTHON_FILE_EXTENSION:
+            return FileType.PYTHON
+        elif file_extension in _XML_FILE_EXTENSIONS:
+            return FileType.XML
+        elif os.path.basename(file_path).startswith('ChangeLog'):
+            return FileType.CHANGELOG
+        elif os.path.basename(file_path) == 'watchlist':
+            return FileType.WATCHLIST
+        elif file_extension == _XCODEPROJ_FILE_EXTENSION:
+            return FileType.XCODEPROJ
+        elif file_extension == _PNG_FILE_EXTENSION:
+            return FileType.PNG
+        elif ((not file_extension and os.path.join("Tools", "Scripts") in file_path) or
+              file_extension in _TEXT_FILE_EXTENSIONS or os.path.basename(file_path) == 'TestExpectations'):
+            return FileType.TEXT
+        else:
+            return FileType.NONE
+
+    def _create_checker(self, file_type, file_path, handle_style_error,
+                        min_confidence):
+        """Instantiate and return a style checker based on file type."""
+        if file_type == FileType.NONE:
+            checker = None
+        elif file_type == FileType.CHANGELOG:
+            should_line_be_checked = None
+            if handle_style_error:
+                should_line_be_checked = handle_style_error.should_line_be_checked
+            checker = ChangeLogChecker(file_path, handle_style_error, should_line_be_checked)
+        elif file_type == FileType.CPP:
+            file_extension = self._file_extension(file_path)
+            checker = CppChecker(file_path, file_extension,
+                                 handle_style_error, min_confidence)
+        elif file_type == FileType.JSON:
+            checker = JSONChecker(file_path, handle_style_error)
+        elif file_type == FileType.PYTHON:
+            checker = PythonChecker(file_path, handle_style_error)
+        elif file_type == FileType.XML:
+            checker = XMLChecker(file_path, handle_style_error)
+        elif file_type == FileType.XCODEPROJ:
+            checker = XcodeProjectFileChecker(file_path, handle_style_error)
+        elif file_type == FileType.PNG:
+            checker = PNGChecker(file_path, handle_style_error)
+        elif file_type == FileType.TEXT:
+            basename = os.path.basename(file_path)
+            if basename == 'TestExpectations':
+                checker = TestExpectationsChecker(file_path, handle_style_error)
+            else:
+                checker = TextChecker(file_path, handle_style_error)
+        elif file_type == FileType.WATCHLIST:
+            checker = WatchListChecker(file_path, handle_style_error)
+        else:
+            raise ValueError('Invalid file type "%(file_type)s": the only valid file types '
+                             "are %(NONE)s, %(CPP)s, and %(TEXT)s."
+                             % {"file_type": file_type,
+                                "NONE": FileType.NONE,
+                                "CPP": FileType.CPP,
+                                "TEXT": FileType.TEXT})
+
+        return checker
+
+    def dispatch(self, file_path, handle_style_error, min_confidence):
+        """Instantiate and return a style checker based on file path."""
+        file_type = self._file_type(file_path)
+
+        checker = self._create_checker(file_type,
+                                       file_path,
+                                       handle_style_error,
+                                       min_confidence)
+        return checker
+
+
+# FIXME: Remove the stderr_write attribute from this class and replace
+#        its use with calls to a logging module logger.
+class StyleProcessorConfiguration(object):
+
+    """Stores configuration values for the StyleProcessor class.
+
+    Attributes:
+      min_confidence: An integer between 1 and 5 inclusive that is the
+                      minimum confidence level of style errors to report.
+
+      max_reports_per_category: The maximum number of errors to report
+                                per category, per file.
+
+      stderr_write: A function that takes a string as a parameter and
+                    serves as stderr.write.
+
+    """
+
+    def __init__(self,
+                 filter_configuration,
+                 max_reports_per_category,
+                 min_confidence,
+                 output_format,
+                 stderr_write):
+        """Create a StyleProcessorConfiguration instance.
+
+        Args:
+          filter_configuration: A FilterConfiguration instance.  The default
+                                is the "empty" filter configuration, which
+                                means that all errors should be checked.
+
+          max_reports_per_category: The maximum number of errors to report
+                                    per category, per file.
+
+          min_confidence: An integer between 1 and 5 inclusive that is the
+                          minimum confidence level of style errors to report.
+                          The default is 1, which reports all style errors.
+
+          output_format: A string that is the output format.  The supported
+                         output formats are "emacs" which emacs can parse
+                         and "vs7" which Microsoft Visual Studio 7 can parse.
+
+          stderr_write: A function that takes a string as a parameter and
+                        serves as stderr.write.
+
+        """
+        self._filter_configuration = filter_configuration
+        self._output_format = output_format
+
+        self.max_reports_per_category = max_reports_per_category
+        self.min_confidence = min_confidence
+        self.stderr_write = stderr_write
+
+    def is_reportable(self, category, confidence_in_error, file_path):
+        """Return whether an error is reportable.
+
+        An error is reportable if both the confidence in the error is
+        at least the minimum confidence level and the current filter
+        says the category should be checked for the given path.
+
+        Args:
+          category: A string that is a style category.
+          confidence_in_error: An integer between 1 and 5 inclusive that is
+                               the application's confidence in the error.
+                               A higher number means greater confidence.
+          file_path: The path of the file being checked
+
+        """
+        if confidence_in_error < self.min_confidence:
+            return False
+
+        return self._filter_configuration.should_check(category, file_path)
+
+    def write_style_error(self,
+                          category,
+                          confidence_in_error,
+                          file_path,
+                          line_number,
+                          message):
+        """Write a style error to the configured stderr."""
+        if self._output_format == 'vs7':
+            format_string = "%s(%s):  %s  [%s] [%d]\n"
+        else:
+            format_string = "%s:%s:  %s  [%s] [%d]\n"
+
+        self.stderr_write(format_string % (file_path,
+                                           line_number,
+                                           message,
+                                           category,
+                                           confidence_in_error))
+
+
+class ProcessorBase(object):
+
+    """The base class for processors of lists of lines."""
+
+    def should_process(self, file_path):
+        """Return whether the file at file_path should be processed.
+
+        The TextFileReader class calls this method prior to reading in
+        the lines of a file.  Use this method, for example, to prevent
+        the style checker from reading binary files into memory.
+
+        """
+        raise NotImplementedError('Subclasses should implement.')
+
+    def process(self, lines, file_path, **kwargs):
+        """Process lines of text read from a file.
+
+        Args:
+          lines: A list of lines of text to process.
+          file_path: The path from which the lines were read.
+          **kwargs: This argument signifies that the process() method of
+                    subclasses of ProcessorBase may support additional
+                    keyword arguments.
+                        For example, a style checker's check() method
+                    may support a "reportable_lines" parameter that represents
+                    the line numbers of the lines for which style errors
+                    should be reported.
+
+        """
+        raise NotImplementedError('Subclasses should implement.')
+
+
+class StyleProcessor(ProcessorBase):
+
+    """A ProcessorBase for checking style.
+
+    Attributes:
+      error_count: An integer that is the total number of reported
+                   errors for the lifetime of this instance.
+
+    """
+
+    def __init__(self, configuration, mock_dispatcher=None,
+                 mock_increment_error_count=None,
+                 mock_carriage_checker_class=None):
+        """Create an instance.
+
+        Args:
+          configuration: A StyleProcessorConfiguration instance.
+          mock_dispatcher: A mock CheckerDispatcher instance.  This
+                           parameter is for unit testing.  Defaults to a
+                           CheckerDispatcher instance.
+          mock_increment_error_count: A mock error-count incrementer.
+          mock_carriage_checker_class: A mock class for checking and
+                                       transforming carriage returns.
+                                       This parameter is for unit testing.
+                                       Defaults to CarriageReturnChecker.
+
+        """
+        if mock_dispatcher is None:
+            dispatcher = CheckerDispatcher()
+        else:
+            dispatcher = mock_dispatcher
+
+        if mock_increment_error_count is None:
+            # The following blank line is present to avoid flagging by pep8.py.
+
+            def increment_error_count():
+                """Increment the total count of reported errors."""
+                self.error_count += 1
+        else:
+            increment_error_count = mock_increment_error_count
+
+        if mock_carriage_checker_class is None:
+            # This needs to be a class rather than an instance since the
+            # process() method instantiates one using parameters.
+            carriage_checker_class = CarriageReturnChecker
+        else:
+            carriage_checker_class = mock_carriage_checker_class
+
+        self.error_count = 0
+
+        self._carriage_checker_class = carriage_checker_class
+        self._configuration = configuration
+        self._dispatcher = dispatcher
+        self._increment_error_count = increment_error_count
+
+    def should_process(self, file_path):
+        """Return whether the file should be checked for style."""
+        if self._dispatcher.should_skip_without_warning(file_path):
+            return False
+        if self._dispatcher.should_skip_with_warning(file_path):
+            _log.warn('File exempt from style guide. Skipping: "%s"'
+                      % file_path)
+            return False
+        return True
+
+    def process(self, lines, file_path, line_numbers=None):
+        """Check the given lines for style.
+
+        Arguments:
+          lines: A list of all lines in the file to check.
+          file_path: The path of the file to process.  If possible, the path
+                     should be relative to the source root.  Otherwise,
+                     path-specific logic may not behave as expected.
+          line_numbers: A list of line numbers of the lines for which
+                        style errors should be reported, or None if errors
+                        for all lines should be reported.  When not None, this
+                        list normally contains the line numbers corresponding
+                        to the modified lines of a patch.
+
+        """
+        _log.debug("Checking style: " + file_path)
+
+        style_error_handler = DefaultStyleErrorHandler(
+            configuration=self._configuration,
+            file_path=file_path,
+            increment_error_count=self._increment_error_count,
+            line_numbers=line_numbers)
+
+        carriage_checker = self._carriage_checker_class(style_error_handler)
+
+        # Check for and remove trailing carriage returns ("\r").
+        if self._dispatcher.should_check_and_strip_carriage_returns(file_path):
+            lines = carriage_checker.check(lines)
+
+        min_confidence = self._configuration.min_confidence
+        checker = self._dispatcher.dispatch(file_path,
+                                            style_error_handler,
+                                            min_confidence)
+
+        if checker is None:
+            raise AssertionError("File should not be checked: '%s'" % file_path)
+
+        _log.debug("Using class: " + checker.__class__.__name__)
+
+        checker.check(lines)
diff --git a/Tools/Scripts/webkitpy/style/checker_unittest.py b/Tools/Scripts/webkitpy/style/checker_unittest.py
new file mode 100755
index 0000000..d834fd5
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checker_unittest.py
@@ -0,0 +1,894 @@
+#!/usr/bin/python
+# -*- coding: utf-8; -*-
+#
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2009 Torch Mobile Inc.
+# Copyright (C) 2009 Apple Inc. All rights reserved.
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+#
+# 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.
+
+"""Unit tests for style.py."""
+
+import logging
+import os
+import unittest
+
+import checker as style
+from webkitpy.common.system.logtesting import LogTesting, TestLogStream
+from checker import _BASE_FILTER_RULES
+from checker import _MAX_REPORTS_PER_CATEGORY
+from checker import _PATH_RULES_SPECIFIER as PATH_RULES_SPECIFIER
+from checker import _all_categories
+from checker import check_webkit_style_configuration
+from checker import check_webkit_style_parser
+from checker import configure_logging
+from checker import CheckerDispatcher
+from checker import ProcessorBase
+from checker import StyleProcessor
+from checker import StyleProcessorConfiguration
+from checkers.changelog import ChangeLogChecker
+from checkers.cpp import CppChecker
+from checkers.jsonchecker import JSONChecker
+from checkers.python import PythonChecker
+from checkers.text import TextChecker
+from checkers.xml import XMLChecker
+from error_handlers import DefaultStyleErrorHandler
+from filter import validate_filter_rules
+from filter import FilterConfiguration
+from optparser import ArgumentParser
+from optparser import CommandOptionValues
+from webkitpy.common.system.logtesting import LoggingTestCase
+from webkitpy.style.filereader import TextFileReader
+
+
+class ConfigureLoggingTestBase(unittest.TestCase):
+
+    """Base class for testing configure_logging().
+
+    Sub-classes should implement:
+
+      is_verbose: The is_verbose value to pass to configure_logging().
+
+    """
+
+    def setUp(self):
+        is_verbose = self.is_verbose
+
+        log_stream = TestLogStream(self)
+
+        # Use a logger other than the root logger or one prefixed with
+        # webkit so as not to conflict with test-webkitpy logging.
+        logger = logging.getLogger("unittest")
+
+        # Configure the test logger not to pass messages along to the
+        # root logger.  This prevents test messages from being
+        # propagated to loggers used by test-webkitpy logging (e.g.
+        # the root logger).
+        logger.propagate = False
+
+        self._handlers = configure_logging(stream=log_stream, logger=logger,
+                                           is_verbose=is_verbose)
+        self._log = logger
+        self._log_stream = log_stream
+
+    def tearDown(self):
+        """Reset logging to its original state.
+
+        This method ensures that the logging configuration set up
+        for a unit test does not affect logging in other unit tests.
+
+        """
+        logger = self._log
+        for handler in self._handlers:
+            logger.removeHandler(handler)
+
+    def assert_log_messages(self, messages):
+        """Assert that the logged messages equal the given messages."""
+        self._log_stream.assertMessages(messages)
+
+
+class ConfigureLoggingTest(ConfigureLoggingTestBase):
+
+    """Tests the configure_logging() function."""
+
+    is_verbose = False
+
+    def test_warning_message(self):
+        self._log.warn("test message")
+        self.assert_log_messages(["WARNING: test message\n"])
+
+    def test_below_warning_message(self):
+        # We test the boundary case of a logging level equal to 29.
+        # In practice, we will probably only be calling log.info(),
+        # which corresponds to a logging level of 20.
+        level = logging.WARNING - 1  # Equals 29.
+        self._log.log(level, "test message")
+        self.assert_log_messages(["test message\n"])
+
+    def test_debug_message(self):
+        self._log.debug("test message")
+        self.assert_log_messages([])
+
+    def test_two_messages(self):
+        self._log.info("message1")
+        self._log.info("message2")
+        self.assert_log_messages(["message1\n", "message2\n"])
+
+
+class ConfigureLoggingVerboseTest(ConfigureLoggingTestBase):
+
+    """Tests the configure_logging() function with is_verbose True."""
+
+    is_verbose = True
+
+    def test_debug_message(self):
+        self._log.debug("test message")
+        self.assert_log_messages(["unittest: DEBUG    test message\n"])
+
+
+class GlobalVariablesTest(unittest.TestCase):
+
+    """Tests validity of the global variables."""
+
+    def _all_categories(self):
+        return _all_categories()
+
+    def defaults(self):
+        return style._check_webkit_style_defaults()
+
+    def test_webkit_base_filter_rules(self):
+        base_filter_rules = _BASE_FILTER_RULES
+        defaults = self.defaults()
+        already_seen = []
+        validate_filter_rules(base_filter_rules, self._all_categories())
+        # Also do some additional checks.
+        for rule in base_filter_rules:
+            # Check no leading or trailing white space.
+            self.assertEquals(rule, rule.strip())
+            # All categories are on by default, so defaults should
+            # begin with -.
+            self.assertTrue(rule.startswith('-'))
+            # Check no rule occurs twice.
+            self.assertFalse(rule in already_seen)
+            already_seen.append(rule)
+
+    def test_defaults(self):
+        """Check that default arguments are valid."""
+        default_options = self.defaults()
+
+        # FIXME: We should not need to call parse() to determine
+        #        whether the default arguments are valid.
+        parser = ArgumentParser(all_categories=self._all_categories(),
+                                base_filter_rules=[],
+                                default_options=default_options)
+        # No need to test the return value here since we test parse()
+        # on valid arguments elsewhere.
+        #
+        # The default options are valid: no error or SystemExit.
+        parser.parse(args=[])
+
+    def test_path_rules_specifier(self):
+        all_categories = self._all_categories()
+        for (sub_paths, path_rules) in PATH_RULES_SPECIFIER:
+            validate_filter_rules(path_rules, self._all_categories())
+
+        config = FilterConfiguration(path_specific=PATH_RULES_SPECIFIER)
+
+        def assertCheck(path, category):
+            """Assert that the given category should be checked."""
+            message = ('Should check category "%s" for path "%s".'
+                       % (category, path))
+            self.assertTrue(config.should_check(category, path))
+
+        def assertNoCheck(path, category):
+            """Assert that the given category should not be checked."""
+            message = ('Should not check category "%s" for path "%s".'
+                       % (category, path))
+            self.assertFalse(config.should_check(category, path), message)
+
+        assertCheck("random_path.cpp",
+                    "build/include")
+        assertNoCheck("Tools/WebKitAPITest/main.cpp",
+                      "build/include")
+        assertCheck("random_path.cpp",
+                    "readability/naming")
+        assertNoCheck("Source/WebKit/gtk/webkit/webkit.h",
+                      "readability/naming")
+        assertNoCheck("Tools/DumpRenderTree/gtk/DumpRenderTree.cpp",
+                      "readability/null")
+        assertNoCheck("Source/WebKit/efl/ewk/ewk_view.h",
+                      "readability/naming")
+        assertNoCheck("Source/WebCore/css/CSSParser.cpp",
+                      "readability/naming")
+
+        # Test if Qt exceptions are indeed working
+        assertCheck("Source/WebKit/qt/Api/qwebpage.cpp",
+                    "readability/braces")
+        assertCheck("Source/WebKit/qt/tests/qwebelement/tst_qwebelement.cpp",
+                    "readability/braces")
+        assertCheck("Source/WebKit/qt/declarative/platformplugin/WebPlugin.cpp",
+                    "readability/braces")
+        assertCheck("Source/WebKit/qt/examples/platformplugin/WebPlugin.cpp",
+                    "readability/braces")
+        assertNoCheck("Source/WebKit/qt/Api/qwebpage.cpp",
+                      "readability/naming")
+        assertNoCheck("Source/WebKit/qt/tests/qwebelement/tst_qwebelement.cpp",
+                      "readability/naming")
+        assertNoCheck("Source/WebKit/qt/declarative/platformplugin/WebPlugin.cpp",
+                      "readability/naming")
+        assertNoCheck("Source/WebKit/qt/examples/platformplugin/WebPlugin.cpp",
+                      "readability/naming")
+
+        assertNoCheck("Tools/MiniBrowser/qt/UrlLoader.cpp",
+                    "build/include")
+
+        assertNoCheck("Source/WebKit2/UIProcess/API/qt",
+                    "readability/parameter_name")
+
+        assertNoCheck("Source/WebCore/ForwardingHeaders/debugger/Debugger.h",
+                      "build/header_guard")
+
+        assertNoCheck("Source/WebCore/platform/graphics/gstreamer/VideoSinkGStreamer.cpp",
+                      "readability/naming")
+
+        # Third-party Python code: webkitpy/thirdparty
+        path = "Tools/Scripts/webkitpy/thirdparty/mock.py"
+        assertNoCheck(path, "build/include")
+        assertNoCheck(path, "pep8/E401")  # A random pep8 category.
+        assertCheck(path, "pep8/W191")
+        assertCheck(path, "pep8/W291")
+        assertCheck(path, "whitespace/carriage_return")
+
+        # Test if the exception for GDBInterface.cpp is in place.
+        assertNoCheck("Source/JavaScriptCore/jit/GDBInterface.cpp",
+                      "readability/naming")
+
+        # Javascript keywords.
+        assertCheck("Source/JavaScriptCore/parser/Keywords.table", "whitespace/carriage_return")
+
+    def test_max_reports_per_category(self):
+        """Check that _MAX_REPORTS_PER_CATEGORY is valid."""
+        all_categories = self._all_categories()
+        for category in _MAX_REPORTS_PER_CATEGORY.iterkeys():
+            self.assertTrue(category in all_categories,
+                            'Key "%s" is not a category' % category)
+
+
+class CheckWebKitStyleFunctionTest(unittest.TestCase):
+
+    """Tests the functions with names of the form check_webkit_style_*."""
+
+    def test_check_webkit_style_configuration(self):
+        # Exercise the code path to make sure the function does not error out.
+        option_values = CommandOptionValues()
+        configuration = check_webkit_style_configuration(option_values)
+
+    def test_check_webkit_style_parser(self):
+        # Exercise the code path to make sure the function does not error out.
+        parser = check_webkit_style_parser()
+
+
+class CheckerDispatcherSkipTest(unittest.TestCase):
+
+    """Tests the "should skip" methods of the CheckerDispatcher class."""
+
+    def setUp(self):
+        self._dispatcher = CheckerDispatcher()
+
+    def test_should_skip_with_warning(self):
+        """Test should_skip_with_warning()."""
+        # Check skipped files.
+        paths_to_skip = [
+           "Source/WebKit/gtk/tests/testatk.c",
+           "Source/WebKit2/UIProcess/API/gtk/webkit2.h",
+           "Source/WebKit2/UIProcess/API/gtk/WebKitWebView.h",
+           "Source/WebKit2/UIProcess/API/gtk/WebKitLoader.h",
+            ]
+
+        for path in paths_to_skip:
+            self.assertTrue(self._dispatcher.should_skip_with_warning(path),
+                            "Checking: " + path)
+
+        # Verify that some files are not skipped.
+        paths_not_to_skip = [
+           "foo.txt",
+           "Source/WebKit2/UIProcess/API/gtk/HelperClass.cpp",
+           "Source/WebKit2/UIProcess/API/gtk/HelperClass.h",
+           "Source/WebKit2/UIProcess/API/gtk/WebKitWebView.cpp",
+           "Source/WebKit2/UIProcess/API/gtk/WebKitWebViewPrivate.h",
+           "Source/WebKit2/UIProcess/API/gtk/tests/WebViewTest.cpp",
+           "Source/WebKit2/UIProcess/API/gtk/tests/WebViewTest.h",
+            ]
+
+        for path in paths_not_to_skip:
+            self.assertFalse(self._dispatcher.should_skip_with_warning(path))
+
+    def _assert_should_skip_without_warning(self, path, is_checker_none,
+                                            expected):
+        # Check the file type before asserting the return value.
+        checker = self._dispatcher.dispatch(file_path=path,
+                                            handle_style_error=None,
+                                            min_confidence=3)
+        message = 'while checking: %s' % path
+        self.assertEquals(checker is None, is_checker_none, message)
+        self.assertEquals(self._dispatcher.should_skip_without_warning(path),
+                          expected, message)
+
+    def test_should_skip_without_warning__true(self):
+        """Test should_skip_without_warning() for True return values."""
+        # Check a file with NONE file type.
+        path = 'foo.asdf'  # Non-sensical file extension.
+        self._assert_should_skip_without_warning(path,
+                                                 is_checker_none=True,
+                                                 expected=True)
+
+        # Check files with non-NONE file type.  These examples must be
+        # drawn from the _SKIPPED_FILES_WITHOUT_WARNING configuration
+        # variable.
+        path = os.path.join('LayoutTests', 'foo.txt')
+        self._assert_should_skip_without_warning(path,
+                                                 is_checker_none=False,
+                                                 expected=True)
+
+    def test_should_skip_without_warning__false(self):
+        """Test should_skip_without_warning() for False return values."""
+        paths = ['foo.txt',
+                 os.path.join('LayoutTests', 'ChangeLog'),
+        ]
+
+        for path in paths:
+            self._assert_should_skip_without_warning(path,
+                                                     is_checker_none=False,
+                                                     expected=False)
+
+
+class CheckerDispatcherCarriageReturnTest(unittest.TestCase):
+    def test_should_check_and_strip_carriage_returns(self):
+        files = {
+            'foo.txt': True,
+            'foo.cpp': True,
+            'foo.vcproj': False,
+            'foo.vsprops': False,
+        }
+
+        dispatcher = CheckerDispatcher()
+        for file_path, expected_result in files.items():
+            self.assertEquals(dispatcher.should_check_and_strip_carriage_returns(file_path), expected_result, 'Checking: %s' % file_path)
+
+
+class CheckerDispatcherDispatchTest(unittest.TestCase):
+
+    """Tests dispatch() method of CheckerDispatcher class."""
+
+    def dispatch(self, file_path):
+        """Call dispatch() with the given file path."""
+        dispatcher = CheckerDispatcher()
+        self.mock_handle_style_error = DefaultStyleErrorHandler('', None, None, [])
+        checker = dispatcher.dispatch(file_path,
+                                      self.mock_handle_style_error,
+                                      min_confidence=3)
+        return checker
+
+    def assert_checker_none(self, file_path):
+        """Assert that the dispatched checker is None."""
+        checker = self.dispatch(file_path)
+        self.assertTrue(checker is None, 'Checking: "%s"' % file_path)
+
+    def assert_checker(self, file_path, expected_class):
+        """Assert the type of the dispatched checker."""
+        checker = self.dispatch(file_path)
+        got_class = checker.__class__
+        self.assertEquals(got_class, expected_class,
+                          'For path "%(file_path)s" got %(got_class)s when '
+                          "expecting %(expected_class)s."
+                          % {"file_path": file_path,
+                             "got_class": got_class,
+                             "expected_class": expected_class})
+
+    def assert_checker_changelog(self, file_path):
+        """Assert that the dispatched checker is a ChangeLogChecker."""
+        self.assert_checker(file_path, ChangeLogChecker)
+
+    def assert_checker_cpp(self, file_path):
+        """Assert that the dispatched checker is a CppChecker."""
+        self.assert_checker(file_path, CppChecker)
+
+    def assert_checker_json(self, file_path):
+        """Assert that the dispatched checker is a JSONChecker."""
+        self.assert_checker(file_path, JSONChecker)
+
+    def assert_checker_python(self, file_path):
+        """Assert that the dispatched checker is a PythonChecker."""
+        self.assert_checker(file_path, PythonChecker)
+
+    def assert_checker_text(self, file_path):
+        """Assert that the dispatched checker is a TextChecker."""
+        self.assert_checker(file_path, TextChecker)
+
+    def assert_checker_xml(self, file_path):
+        """Assert that the dispatched checker is a XMLChecker."""
+        self.assert_checker(file_path, XMLChecker)
+
+    def test_changelog_paths(self):
+        """Test paths that should be checked as ChangeLog."""
+        paths = [
+                 "ChangeLog",
+                 "ChangeLog-2009-06-16",
+                 os.path.join("Source", "WebCore", "ChangeLog"),
+                 ]
+
+        for path in paths:
+            self.assert_checker_changelog(path)
+
+        # Check checker attributes on a typical input.
+        file_path = "ChangeLog"
+        self.assert_checker_changelog(file_path)
+        checker = self.dispatch(file_path)
+        self.assertEquals(checker.file_path, file_path)
+        self.assertEquals(checker.handle_style_error,
+                          self.mock_handle_style_error)
+
+    def test_cpp_paths(self):
+        """Test paths that should be checked as C++."""
+        paths = [
+            "-",
+            "foo.c",
+            "foo.cpp",
+            "foo.h",
+            ]
+
+        for path in paths:
+            self.assert_checker_cpp(path)
+
+        # Check checker attributes on a typical input.
+        file_base = "foo"
+        file_extension = "c"
+        file_path = file_base + "." + file_extension
+        self.assert_checker_cpp(file_path)
+        checker = self.dispatch(file_path)
+        self.assertEquals(checker.file_extension, file_extension)
+        self.assertEquals(checker.file_path, file_path)
+        self.assertEquals(checker.handle_style_error, self.mock_handle_style_error)
+        self.assertEquals(checker.min_confidence, 3)
+        # Check "-" for good measure.
+        file_base = "-"
+        file_extension = ""
+        file_path = file_base
+        self.assert_checker_cpp(file_path)
+        checker = self.dispatch(file_path)
+        self.assertEquals(checker.file_extension, file_extension)
+        self.assertEquals(checker.file_path, file_path)
+
+    def test_json_paths(self):
+        """Test paths that should be checked as JSON."""
+        paths = [
+           "Source/WebCore/inspector/Inspector.json",
+           "Tools/BuildSlaveSupport/build.webkit.org-config/config.json",
+        ]
+
+        for path in paths:
+            self.assert_checker_json(path)
+
+        # Check checker attributes on a typical input.
+        file_base = "foo"
+        file_extension = "json"
+        file_path = file_base + "." + file_extension
+        self.assert_checker_json(file_path)
+        checker = self.dispatch(file_path)
+        self.assertEquals(checker._handle_style_error,
+                          self.mock_handle_style_error)
+
+    def test_python_paths(self):
+        """Test paths that should be checked as Python."""
+        paths = [
+           "foo.py",
+           "Tools/Scripts/modules/text_style.py",
+        ]
+
+        for path in paths:
+            self.assert_checker_python(path)
+
+        # Check checker attributes on a typical input.
+        file_base = "foo"
+        file_extension = "css"
+        file_path = file_base + "." + file_extension
+        self.assert_checker_text(file_path)
+        checker = self.dispatch(file_path)
+        self.assertEquals(checker.file_path, file_path)
+        self.assertEquals(checker.handle_style_error,
+                          self.mock_handle_style_error)
+
+    def test_text_paths(self):
+        """Test paths that should be checked as text."""
+        paths = [
+           "foo.ac",
+           "foo.cc",
+           "foo.cgi",
+           "foo.css",
+           "foo.exp",
+           "foo.flex",
+           "foo.gyp",
+           "foo.gypi",
+           "foo.html",
+           "foo.idl",
+           "foo.in",
+           "foo.js",
+           "foo.mm",
+           "foo.php",
+           "foo.pl",
+           "foo.pm",
+           "foo.pri",
+           "foo.pro",
+           "foo.rb",
+           "foo.sh",
+           "foo.txt",
+           "foo.wm",
+           "foo.xhtml",
+           "foo.y",
+           os.path.join("Source", "WebCore", "inspector", "front-end", "inspector.js"),
+           os.path.join("Tools", "Scripts", "check-webkit-style"),
+        ]
+
+        for path in paths:
+            self.assert_checker_text(path)
+
+        # Check checker attributes on a typical input.
+        file_base = "foo"
+        file_extension = "css"
+        file_path = file_base + "." + file_extension
+        self.assert_checker_text(file_path)
+        checker = self.dispatch(file_path)
+        self.assertEquals(checker.file_path, file_path)
+        self.assertEquals(checker.handle_style_error, self.mock_handle_style_error)
+
+    def test_xml_paths(self):
+        """Test paths that should be checked as XML."""
+        paths = [
+           "Source/WebCore/WebCore.vcproj/WebCore.vcproj",
+           "WebKitLibraries/win/tools/vsprops/common.vsprops",
+        ]
+
+        for path in paths:
+            self.assert_checker_xml(path)
+
+        # Check checker attributes on a typical input.
+        file_base = "foo"
+        file_extension = "vcproj"
+        file_path = file_base + "." + file_extension
+        self.assert_checker_xml(file_path)
+        checker = self.dispatch(file_path)
+        self.assertEquals(checker._handle_style_error,
+                          self.mock_handle_style_error)
+
+    def test_none_paths(self):
+        """Test paths that have no file type.."""
+        paths = [
+           "Makefile",
+           "foo.asdf",  # Non-sensical file extension.
+           "foo.exe",
+            ]
+
+        for path in paths:
+            self.assert_checker_none(path)
+
+
+class StyleProcessorConfigurationTest(unittest.TestCase):
+
+    """Tests the StyleProcessorConfiguration class."""
+
+    def setUp(self):
+        self._error_messages = []
+        """The messages written to _mock_stderr_write() of this class."""
+
+    def _mock_stderr_write(self, message):
+        self._error_messages.append(message)
+
+    def _style_checker_configuration(self, output_format="vs7"):
+        """Return a StyleProcessorConfiguration instance for testing."""
+        base_rules = ["-whitespace", "+whitespace/tab"]
+        filter_configuration = FilterConfiguration(base_rules=base_rules)
+
+        return StyleProcessorConfiguration(
+                   filter_configuration=filter_configuration,
+                   max_reports_per_category={"whitespace/newline": 1},
+                   min_confidence=3,
+                   output_format=output_format,
+                   stderr_write=self._mock_stderr_write)
+
+    def test_init(self):
+        """Test the __init__() method."""
+        configuration = self._style_checker_configuration()
+
+        # Check that __init__ sets the "public" data attributes correctly.
+        self.assertEquals(configuration.max_reports_per_category,
+                          {"whitespace/newline": 1})
+        self.assertEquals(configuration.stderr_write, self._mock_stderr_write)
+        self.assertEquals(configuration.min_confidence, 3)
+
+    def test_is_reportable(self):
+        """Test the is_reportable() method."""
+        config = self._style_checker_configuration()
+
+        self.assertTrue(config.is_reportable("whitespace/tab", 3, "foo.txt"))
+
+        # Test the confidence check code path by varying the confidence.
+        self.assertFalse(config.is_reportable("whitespace/tab", 2, "foo.txt"))
+
+        # Test the category check code path by varying the category.
+        self.assertFalse(config.is_reportable("whitespace/line", 4, "foo.txt"))
+
+    def _call_write_style_error(self, output_format):
+        config = self._style_checker_configuration(output_format=output_format)
+        config.write_style_error(category="whitespace/tab",
+                                 confidence_in_error=5,
+                                 file_path="foo.h",
+                                 line_number=100,
+                                 message="message")
+
+    def test_write_style_error_emacs(self):
+        """Test the write_style_error() method."""
+        self._call_write_style_error("emacs")
+        self.assertEquals(self._error_messages,
+                          ["foo.h:100:  message  [whitespace/tab] [5]\n"])
+
+    def test_write_style_error_vs7(self):
+        """Test the write_style_error() method."""
+        self._call_write_style_error("vs7")
+        self.assertEquals(self._error_messages,
+                          ["foo.h(100):  message  [whitespace/tab] [5]\n"])
+
+
+class StyleProcessor_EndToEndTest(LoggingTestCase):
+
+    """Test the StyleProcessor class with an emphasis on end-to-end tests."""
+
+    def setUp(self):
+        LoggingTestCase.setUp(self)
+        self._messages = []
+
+    def _mock_stderr_write(self, message):
+        """Save a message so it can later be asserted."""
+        self._messages.append(message)
+
+    def test_init(self):
+        """Test __init__ constructor."""
+        configuration = StyleProcessorConfiguration(
+                            filter_configuration=FilterConfiguration(),
+                            max_reports_per_category={},
+                            min_confidence=3,
+                            output_format="vs7",
+                            stderr_write=self._mock_stderr_write)
+        processor = StyleProcessor(configuration)
+
+        self.assertEquals(processor.error_count, 0)
+        self.assertEquals(self._messages, [])
+
+    def test_process(self):
+        configuration = StyleProcessorConfiguration(
+                            filter_configuration=FilterConfiguration(),
+                            max_reports_per_category={},
+                            min_confidence=3,
+                            output_format="vs7",
+                            stderr_write=self._mock_stderr_write)
+        processor = StyleProcessor(configuration)
+
+        processor.process(lines=['line1', 'Line with tab:\t'],
+                          file_path='foo.txt')
+        self.assertEquals(processor.error_count, 1)
+        expected_messages = ['foo.txt(2):  Line contains tab character.  '
+                             '[whitespace/tab] [5]\n']
+        self.assertEquals(self._messages, expected_messages)
+
+
+class StyleProcessor_CodeCoverageTest(LoggingTestCase):
+
+    """Test the StyleProcessor class with an emphasis on code coverage.
+
+    This class makes heavy use of mock objects.
+
+    """
+
+    class MockDispatchedChecker(object):
+
+        """A mock checker dispatched by the MockDispatcher."""
+
+        def __init__(self, file_path, min_confidence, style_error_handler):
+            self.file_path = file_path
+            self.min_confidence = min_confidence
+            self.style_error_handler = style_error_handler
+
+        def check(self, lines):
+            self.lines = lines
+
+    class MockDispatcher(object):
+
+        """A mock CheckerDispatcher class."""
+
+        def __init__(self):
+            self.dispatched_checker = None
+
+        def should_skip_with_warning(self, file_path):
+            return file_path.endswith('skip_with_warning.txt')
+
+        def should_skip_without_warning(self, file_path):
+            return file_path.endswith('skip_without_warning.txt')
+
+        def should_check_and_strip_carriage_returns(self, file_path):
+            return not file_path.endswith('carriage_returns_allowed.txt')
+
+        def dispatch(self, file_path, style_error_handler, min_confidence):
+            if file_path.endswith('do_not_process.txt'):
+                return None
+
+            checker = StyleProcessor_CodeCoverageTest.MockDispatchedChecker(
+                          file_path,
+                          min_confidence,
+                          style_error_handler)
+
+            # Save the dispatched checker so the current test case has a
+            # way to access and check it.
+            self.dispatched_checker = checker
+
+            return checker
+
+    def setUp(self):
+        LoggingTestCase.setUp(self)
+        # We can pass an error-message swallower here because error message
+        # output is tested instead in the end-to-end test case above.
+        configuration = StyleProcessorConfiguration(
+                            filter_configuration=FilterConfiguration(),
+                            max_reports_per_category={"whitespace/newline": 1},
+                            min_confidence=3,
+                            output_format="vs7",
+                            stderr_write=self._swallow_stderr_message)
+
+        mock_carriage_checker_class = self._create_carriage_checker_class()
+        mock_dispatcher = self.MockDispatcher()
+        # We do not need to use a real incrementer here because error-count
+        # incrementing is tested instead in the end-to-end test case above.
+        mock_increment_error_count = self._do_nothing
+
+        processor = StyleProcessor(configuration=configuration,
+                        mock_carriage_checker_class=mock_carriage_checker_class,
+                        mock_dispatcher=mock_dispatcher,
+                        mock_increment_error_count=mock_increment_error_count)
+
+        self._configuration = configuration
+        self._mock_dispatcher = mock_dispatcher
+        self._processor = processor
+
+    def _do_nothing(self):
+        # We provide this function so the caller can pass it to the
+        # StyleProcessor constructor.  This lets us assert the equality of
+        # the DefaultStyleErrorHandler instance generated by the process()
+        # method with an expected instance.
+        pass
+
+    def _swallow_stderr_message(self, message):
+        """Swallow a message passed to stderr.write()."""
+        # This is a mock stderr.write() for passing to the constructor
+        # of the StyleProcessorConfiguration class.
+        pass
+
+    def _create_carriage_checker_class(self):
+
+        # Create a reference to self with a new name so its name does not
+        # conflict with the self introduced below.
+        test_case = self
+
+        class MockCarriageChecker(object):
+
+            """A mock carriage-return checker."""
+
+            def __init__(self, style_error_handler):
+                self.style_error_handler = style_error_handler
+
+                # This gives the current test case access to the
+                # instantiated carriage checker.
+                test_case.carriage_checker = self
+
+            def check(self, lines):
+                # Save the lines so the current test case has a way to access
+                # and check them.
+                self.lines = lines
+
+                return lines
+
+        return MockCarriageChecker
+
+    def test_should_process__skip_without_warning(self):
+        """Test should_process() for a skip-without-warning file."""
+        file_path = "foo/skip_without_warning.txt"
+
+        self.assertFalse(self._processor.should_process(file_path))
+
+    def test_should_process__skip_with_warning(self):
+        """Test should_process() for a skip-with-warning file."""
+        file_path = "foo/skip_with_warning.txt"
+
+        self.assertFalse(self._processor.should_process(file_path))
+
+        self.assertLog(['WARNING: File exempt from style guide. '
+                        'Skipping: "foo/skip_with_warning.txt"\n'])
+
+    def test_should_process__true_result(self):
+        """Test should_process() for a file that should be processed."""
+        file_path = "foo/skip_process.txt"
+
+        self.assertTrue(self._processor.should_process(file_path))
+
+    def test_process__checker_dispatched(self):
+        """Test the process() method for a path with a dispatched checker."""
+        file_path = 'foo.txt'
+        lines = ['line1', 'line2']
+        line_numbers = [100]
+
+        expected_error_handler = DefaultStyleErrorHandler(
+            configuration=self._configuration,
+            file_path=file_path,
+            increment_error_count=self._do_nothing,
+            line_numbers=line_numbers)
+
+        self._processor.process(lines=lines,
+                                file_path=file_path,
+                                line_numbers=line_numbers)
+
+        # Check that the carriage-return checker was instantiated correctly
+        # and was passed lines correctly.
+        carriage_checker = self.carriage_checker
+        self.assertEquals(carriage_checker.style_error_handler,
+                          expected_error_handler)
+        self.assertEquals(carriage_checker.lines, ['line1', 'line2'])
+
+        # Check that the style checker was dispatched correctly and was
+        # passed lines correctly.
+        checker = self._mock_dispatcher.dispatched_checker
+        self.assertEquals(checker.file_path, 'foo.txt')
+        self.assertEquals(checker.min_confidence, 3)
+        self.assertEquals(checker.style_error_handler, expected_error_handler)
+
+        self.assertEquals(checker.lines, ['line1', 'line2'])
+
+    def test_process__no_checker_dispatched(self):
+        """Test the process() method for a path with no dispatched checker."""
+        path = os.path.join('foo', 'do_not_process.txt')
+        self.assertRaises(AssertionError, self._processor.process,
+                          lines=['line1', 'line2'], file_path=path,
+                          line_numbers=[100])
+
+    def test_process__carriage_returns_not_stripped(self):
+        """Test that carriage returns aren't stripped from files that are allowed to contain them."""
+        file_path = 'carriage_returns_allowed.txt'
+        lines = ['line1\r', 'line2\r']
+        line_numbers = [100]
+        self._processor.process(lines=lines,
+                                file_path=file_path,
+                                line_numbers=line_numbers)
+        # The carriage return checker should never have been invoked, and so
+        # should not have saved off any lines.
+        self.assertFalse(hasattr(self.carriage_checker, 'lines'))
diff --git a/Tools/Scripts/webkitpy/style/checkers/__init__.py b/Tools/Scripts/webkitpy/style/checkers/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/style/checkers/changelog.py b/Tools/Scripts/webkitpy/style/checkers/changelog.py
new file mode 100644
index 0000000..a096d3f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/changelog.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2011 Patrick Gansterer <paroga@paroga.com>
+#
+# 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 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.
+
+"""Checks WebKit style for ChangeLog files."""
+
+import re
+from common import TabChecker
+from webkitpy.common.checkout.changelog import parse_bug_id_from_changelog
+
+
+class ChangeLogChecker(object):
+    """Processes text lines for checking style."""
+
+    categories = set(['changelog/bugnumber', 'changelog/filechangedescriptionwhitespace'])
+
+    def __init__(self, file_path, handle_style_error, should_line_be_checked):
+        self.file_path = file_path
+        self.handle_style_error = handle_style_error
+        self.should_line_be_checked = should_line_be_checked
+        self._tab_checker = TabChecker(file_path, handle_style_error)
+
+    def check_entry(self, first_line_checked, entry_lines):
+        if not entry_lines:
+            return
+        for line in entry_lines:
+            if parse_bug_id_from_changelog(line):
+                break
+            if re.search("Unreviewed", line, re.IGNORECASE):
+                break
+            if re.search("build", line, re.IGNORECASE) and re.search("fix", line, re.IGNORECASE):
+                break
+        else:
+            self.handle_style_error(first_line_checked,
+                                    "changelog/bugnumber", 5,
+                                    "ChangeLog entry has no bug number")
+        # check file change descriptions for style violations
+        line_no = first_line_checked - 1
+        for line in entry_lines:
+            line_no = line_no + 1
+            # filter file change descriptions
+            if not re.match('\s*\*\s', line):
+                continue
+            if re.search(':\s*$', line) or re.search(':\s', line):
+                continue
+            self.handle_style_error(line_no,
+                                    "changelog/filechangedescriptionwhitespace", 5,
+                                    "Need whitespace between colon and description")
+
+        # check for a lingering "No new tests. (OOPS!)" left over from prepare-changeLog.
+        line_no = first_line_checked - 1
+        for line in entry_lines:
+            line_no = line_no + 1
+            if re.match('\s*No new tests. \(OOPS!\)$', line):
+                self.handle_style_error(line_no,
+                                        "changelog/nonewtests", 5,
+                                        "You should remove the 'No new tests' and either add and list tests, or explain why no new tests were possible.")
+
+    def check(self, lines):
+        self._tab_checker.check(lines)
+        first_line_checked = 0
+        entry_lines = []
+
+        for line_index, line in enumerate(lines):
+            if not self.should_line_be_checked(line_index + 1):
+                # If we transitioned from finding changed lines to
+                # unchanged lines, then we are done.
+                if first_line_checked:
+                    break
+                continue
+            if not first_line_checked:
+                first_line_checked = line_index + 1
+            entry_lines.append(line)
+
+        self.check_entry(first_line_checked, entry_lines)
diff --git a/Tools/Scripts/webkitpy/style/checkers/changelog_unittest.py b/Tools/Scripts/webkitpy/style/checkers/changelog_unittest.py
new file mode 100644
index 0000000..9fe8a60
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/changelog_unittest.py
@@ -0,0 +1,187 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2010 Apple Inc. All rights reserved.
+# Copyright (C) 2011 Patrick Gansterer <paroga@paroga.com>
+#
+# 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.
+
+"""Unit test for changelog.py."""
+
+import changelog
+import unittest
+
+
+class ChangeLogCheckerTest(unittest.TestCase):
+    """Tests ChangeLogChecker class."""
+
+    def assert_no_error(self, lines_to_check, changelog_data):
+        def handle_style_error(line_number, category, confidence, message):
+            self.fail('Unexpected error: %d %s %d %s for\n%s' % (line_number, category, confidence, message, changelog_data))
+        self.lines_to_check = set(lines_to_check)
+        checker = changelog.ChangeLogChecker('ChangeLog', handle_style_error, self.mock_should_line_be_checked)
+        checker.check(changelog_data.split('\n'))
+
+    def assert_error(self, expected_line_number, lines_to_check, expected_category, changelog_data):
+        self.had_error = False
+
+        def handle_style_error(line_number, category, confidence, message):
+            self.had_error = True
+            self.assertEquals(expected_line_number, line_number)
+            self.assertEquals(expected_category, category)
+        self.lines_to_check = set(lines_to_check)
+        checker = changelog.ChangeLogChecker('ChangeLog', handle_style_error, self.mock_should_line_be_checked)
+        checker.check(changelog_data.split('\n'))
+        self.assertTrue(self.had_error)
+
+    def mock_handle_style_error(self):
+        pass
+
+    def mock_should_line_be_checked(self, line_number):
+        return line_number in self.lines_to_check
+
+    def test_init(self):
+        checker = changelog.ChangeLogChecker('ChangeLog', self.mock_handle_style_error, self.mock_should_line_be_checked)
+        self.assertEquals(checker.file_path, 'ChangeLog')
+        self.assertEquals(checker.handle_style_error, self.mock_handle_style_error)
+        self.assertEquals(checker.should_line_be_checked, self.mock_should_line_be_checked)
+
+    def test_missing_bug_number(self):
+        self.assert_error(1, range(1, 20), 'changelog/bugnumber',
+                          '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                          '\n'
+                          '        Example bug\n')
+        self.assert_error(1, range(1, 20), 'changelog/bugnumber',
+                          '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                          '\n'
+                          '        Example bug\n'
+                          '        http://bugs.webkit.org/show_bug.cgi?id=\n')
+        self.assert_error(1, range(1, 20), 'changelog/bugnumber',
+                          '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                          '\n'
+                          '        Example bug\n'
+                          '        https://bugs.webkit.org/show_bug.cgi?id=\n')
+        self.assert_error(1, range(1, 20), 'changelog/bugnumber',
+                          '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                          '\n'
+                          '        Example bug\n'
+                          '        http://webkit.org/b/\n')
+        self.assert_error(1, range(1, 20), 'changelog/bugnumber',
+                          '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                          '\n'
+                          '        Example bug'
+                          '\n'
+                          '        http://trac.webkit.org/changeset/12345\n')
+        self.assert_error(2, range(2, 5), 'changelog/bugnumber',
+                          '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                          '        Example bug\n'
+                          '        https://bugs.webkit.org/show_bug.cgi\n'
+                          '\n'
+                          '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                          '        Another change\n')
+        self.assert_error(2, range(2, 6), 'changelog/bugnumber',
+                          '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                          '        Example bug\n'
+                          '        More text about bug.\n'
+                          '\n'
+                          '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                          '\n'
+                          '        No bug in this change.\n')
+
+    def test_file_descriptions(self):
+        self.assert_error(5, range(1, 20), 'changelog/filechangedescriptionwhitespace',
+                          '2011-01-01 Dmitry Lomov  <dslomov@google.com>\n'
+                          '        ExampleBug\n'
+                          '        http://bugs.webkit.org/show_bug.cgi?id=12345\n'
+                          '\n'
+                          '        *  Source/Tools/random-script.py:Fixed')
+        self.assert_error(6, range(1, 20), 'changelog/filechangedescriptionwhitespace',
+                          '2011-01-01 Dmitry Lomov  <dslomov@google.com>\n'
+                          '        ExampleBug\n'
+                          '        http://bugs.webkit.org/show_bug.cgi?id=12345\n'
+                          '\n'
+                          '        *  Source/Tools/another-file: Done\n'
+                          '        *  Source/Tools/random-script.py:Fixed\n'
+                          '        *  Source/Tools/one-morefile:\n')
+
+    def test_no_new_tests(self):
+        self.assert_error(5, range(1, 20), 'changelog/nonewtests',
+                          '2011-01-01 Dmitry Lomov  <dslomov@google.com>\n'
+                          '        ExampleBug\n'
+                          '        http://bugs.webkit.org/show_bug.cgi?id=12345\n'
+                          '\n'
+                          '        No new tests. (OOPS!)\n'
+                          '        *  Source/Tools/random-script.py: Fixed')
+
+    def test_no_error(self):
+        self.assert_no_error([],
+                             '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                             '\n'
+                             '        Example ChangeLog entry out of range\n'
+                             '        http://example.com/\n')
+        self.assert_no_error([],
+                             '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                             '\n'
+                             '        Example bug\n'
+                             '        http://bugs.webkit.org/show_bug.cgi?id=12345\n')
+        self.assert_no_error(range(1, 20),
+                             '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                             '\n'
+                             '        Example bug\n'
+                             '        http://bugs.webkit.org/show_bug.cgi?id=12345\n')
+        self.assert_no_error(range(1, 20),
+                             '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                             '\n'
+                             '        Example bug\n'
+                             '        https://bugs.webkit.org/show_bug.cgi?id=12345\n')
+        self.assert_no_error(range(1, 20),
+                             '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                             '\n'
+                             '        Example bug\n'
+                             '        http://webkit.org/b/12345\n')
+        self.assert_no_error(range(1, 20),
+                             '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                             '\n'
+                             '        Unreview build fix for r12345.\n')
+        self.assert_no_error(range(1, 20),
+                             '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                             '\n'
+                             '        Fix build after a bad change.\n')
+        self.assert_no_error(range(1, 20),
+                             '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                             '\n'
+                             '        Fix example port build.\n')
+        self.assert_no_error(range(2, 6),
+                             '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                             '        Example bug\n'
+                             '        https://bugs.webkit.org/show_bug.cgi?id=12345\n'
+                             '\n'
+                             '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                             '        No bug here!\n')
+        self.assert_no_error(range(1, 20),
+                             '2011-01-01  Patrick Gansterer  <paroga@paroga.com>\n'
+                             '        Example bug\n'
+                             '        https://bugs.webkit.org/show_bug.cgi?id=12345\n'
+                             '        * Source/WebKit/foo.cpp:    \n'
+                             '        * Source/WebKit/bar.cpp:\n'
+                             '        * Source/WebKit/foobar.cpp: Description\n')
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/style/checkers/common.py b/Tools/Scripts/webkitpy/style/checkers/common.py
new file mode 100644
index 0000000..76aa956
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/common.py
@@ -0,0 +1,74 @@
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+"""Supports style checking not specific to any one file type."""
+
+
+# FIXME: Test this list in the same way that the list of CppChecker
+#        categories is tested, for example by checking that all of its
+#        elements appear in the unit tests. This should probably be done
+#        after moving the relevant cpp_unittest.ErrorCollector code
+#        into a shared location and refactoring appropriately.
+categories = set([
+    "whitespace/carriage_return",
+    "whitespace/tab"])
+
+
+class CarriageReturnChecker(object):
+
+    """Supports checking for and handling carriage returns."""
+
+    def __init__(self, handle_style_error):
+        self._handle_style_error = handle_style_error
+
+    def check(self, lines):
+        """Check for and strip trailing carriage returns from lines."""
+        for line_number in range(len(lines)):
+            if not lines[line_number].endswith("\r"):
+                continue
+
+            self._handle_style_error(line_number + 1,  # Correct for offset.
+                                     "whitespace/carriage_return",
+                                     1,
+                                     "One or more unexpected \\r (^M) found; "
+                                     "better to use only a \\n")
+
+            lines[line_number] = lines[line_number].rstrip("\r")
+
+        return lines
+
+
+class TabChecker(object):
+
+    """Supports checking for and handling tabs."""
+
+    def __init__(self, file_path, handle_style_error):
+        self.file_path = file_path
+        self.handle_style_error = handle_style_error
+
+    def check(self, lines):
+        # FIXME: share with cpp_style.
+        for line_number, line in enumerate(lines):
+            if "\t" in line:
+                self.handle_style_error(line_number + 1,
+                                        "whitespace/tab", 5,
+                                        "Line contains tab character.")
diff --git a/Tools/Scripts/webkitpy/style/checkers/common_unittest.py b/Tools/Scripts/webkitpy/style/checkers/common_unittest.py
new file mode 100644
index 0000000..1fe1263
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/common_unittest.py
@@ -0,0 +1,124 @@
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+"""Unit tests for common.py."""
+
+import unittest
+
+from common import CarriageReturnChecker
+from common import TabChecker
+
+# FIXME: The unit tests for the cpp, text, and common checkers should
+#        share supporting test code. This can include, for example, the
+#        mock style error handling code and the code to check that all
+#        of a checker's categories are covered by the unit tests.
+#        Such shared code can be located in a shared test file, perhaps
+#        even this file.
+class CarriageReturnCheckerTest(unittest.TestCase):
+
+    """Tests check_no_carriage_return()."""
+
+    _category = "whitespace/carriage_return"
+    _confidence = 1
+    _expected_message = ("One or more unexpected \\r (^M) found; "
+                         "better to use only a \\n")
+
+    def setUp(self):
+        self._style_errors = [] # The list of accumulated style errors.
+
+    def _mock_style_error_handler(self, line_number, category, confidence,
+                                  message):
+        """Append the error information to the list of style errors."""
+        error = (line_number, category, confidence, message)
+        self._style_errors.append(error)
+
+    def assert_carriage_return(self, input_lines, expected_lines, error_lines):
+        """Process the given line and assert that the result is correct."""
+        handle_style_error = self._mock_style_error_handler
+
+        checker = CarriageReturnChecker(handle_style_error)
+        output_lines = checker.check(input_lines)
+
+        # Check both the return value and error messages.
+        self.assertEquals(output_lines, expected_lines)
+
+        expected_errors = [(line_number, self._category, self._confidence,
+                            self._expected_message)
+                           for line_number in error_lines]
+        self.assertEquals(self._style_errors, expected_errors)
+
+    def test_ends_with_carriage(self):
+        self.assert_carriage_return(["carriage return\r"],
+                                    ["carriage return"],
+                                    [1])
+
+    def test_ends_with_nothing(self):
+        self.assert_carriage_return(["no carriage return"],
+                                    ["no carriage return"],
+                                    [])
+
+    def test_ends_with_newline(self):
+        self.assert_carriage_return(["no carriage return\n"],
+                                    ["no carriage return\n"],
+                                    [])
+
+    def test_carriage_in_middle(self):
+        # The CarriageReturnChecker checks only the final character
+        # of each line.
+        self.assert_carriage_return(["carriage\r in a string"],
+                                    ["carriage\r in a string"],
+                                    [])
+
+    def test_multiple_errors(self):
+        self.assert_carriage_return(["line1", "line2\r", "line3\r"],
+                                    ["line1", "line2", "line3"],
+                                    [2, 3])
+
+
+class TabCheckerTest(unittest.TestCase):
+
+    """Tests for TabChecker."""
+
+    def assert_tab(self, input_lines, error_lines):
+        """Assert when the given lines contain tabs."""
+        self._error_lines = []
+
+        def style_error_handler(line_number, category, confidence, message):
+            self.assertEqual(category, 'whitespace/tab')
+            self.assertEqual(confidence, 5)
+            self.assertEqual(message, 'Line contains tab character.')
+            self._error_lines.append(line_number)
+
+        checker = TabChecker('', style_error_handler)
+        checker.check(input_lines)
+        self.assertEquals(self._error_lines, error_lines)
+
+    def test_notab(self):
+        self.assert_tab([''], [])
+        self.assert_tab(['foo', 'bar'], [])
+
+    def test_tab(self):
+        self.assert_tab(['\tfoo'], [1])
+        self.assert_tab(['line1', '\tline2', 'line3\t'], [2, 3])
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/style/checkers/cpp.py b/Tools/Scripts/webkitpy/style/checkers/cpp.py
new file mode 100644
index 0000000..a1447e2
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/cpp.py
@@ -0,0 +1,3682 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009, 2010, 2012 Google Inc. All rights reserved.
+# Copyright (C) 2009 Torch Mobile Inc.
+# Copyright (C) 2009 Apple Inc. All rights reserved.
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# This is the modified version of Google's cpplint. The original code is
+# http://google-styleguide.googlecode.com/svn/trunk/cpplint/cpplint.py
+
+"""Support for check-webkit-style."""
+
+import codecs
+import math  # for log
+import os
+import os.path
+import re
+import sre_compile
+import string
+import sys
+import unicodedata
+
+from webkitpy.common.memoized import memoized
+
+# The key to use to provide a class to fake loading a header file.
+INCLUDE_IO_INJECTION_KEY = 'include_header_io'
+
+# Headers that we consider STL headers.
+_STL_HEADERS = frozenset([
+    'algobase.h', 'algorithm', 'alloc.h', 'bitset', 'deque', 'exception',
+    'function.h', 'functional', 'hash_map', 'hash_map.h', 'hash_set',
+    'hash_set.h', 'iterator', 'list', 'list.h', 'map', 'memory', 'pair.h',
+    'pthread_alloc', 'queue', 'set', 'set.h', 'sstream', 'stack',
+    'stl_alloc.h', 'stl_relops.h', 'type_traits.h',
+    'utility', 'vector', 'vector.h',
+    ])
+
+
+# Non-STL C++ system headers.
+_CPP_HEADERS = frozenset([
+    'algo.h', 'builtinbuf.h', 'bvector.h', 'cassert', 'cctype',
+    'cerrno', 'cfloat', 'ciso646', 'climits', 'clocale', 'cmath',
+    'complex', 'complex.h', 'csetjmp', 'csignal', 'cstdarg', 'cstddef',
+    'cstdio', 'cstdlib', 'cstring', 'ctime', 'cwchar', 'cwctype',
+    'defalloc.h', 'deque.h', 'editbuf.h', 'exception', 'fstream',
+    'fstream.h', 'hashtable.h', 'heap.h', 'indstream.h', 'iomanip',
+    'iomanip.h', 'ios', 'iosfwd', 'iostream', 'iostream.h', 'istream.h',
+    'iterator.h', 'limits', 'map.h', 'multimap.h', 'multiset.h',
+    'numeric', 'ostream.h', 'parsestream.h', 'pfstream.h', 'PlotFile.h',
+    'procbuf.h', 'pthread_alloc.h', 'rope', 'rope.h', 'ropeimpl.h',
+    'SFile.h', 'slist', 'slist.h', 'stack.h', 'stdexcept',
+    'stdiostream.h', 'streambuf.h', 'stream.h', 'strfile.h', 'string',
+    'strstream', 'strstream.h', 'tempbuf.h', 'tree.h', 'typeinfo', 'valarray',
+    ])
+
+
+# Assertion macros.  These are defined in base/logging.h and
+# testing/base/gunit.h.  Note that the _M versions need to come first
+# for substring matching to work.
+_CHECK_MACROS = [
+    'DCHECK', 'CHECK',
+    'EXPECT_TRUE_M', 'EXPECT_TRUE',
+    'ASSERT_TRUE_M', 'ASSERT_TRUE',
+    'EXPECT_FALSE_M', 'EXPECT_FALSE',
+    'ASSERT_FALSE_M', 'ASSERT_FALSE',
+    ]
+
+# Replacement macros for CHECK/DCHECK/EXPECT_TRUE/EXPECT_FALSE
+_CHECK_REPLACEMENT = dict([(m, {}) for m in _CHECK_MACROS])
+
+for op, replacement in [('==', 'EQ'), ('!=', 'NE'),
+                        ('>=', 'GE'), ('>', 'GT'),
+                        ('<=', 'LE'), ('<', 'LT')]:
+    _CHECK_REPLACEMENT['DCHECK'][op] = 'DCHECK_%s' % replacement
+    _CHECK_REPLACEMENT['CHECK'][op] = 'CHECK_%s' % replacement
+    _CHECK_REPLACEMENT['EXPECT_TRUE'][op] = 'EXPECT_%s' % replacement
+    _CHECK_REPLACEMENT['ASSERT_TRUE'][op] = 'ASSERT_%s' % replacement
+    _CHECK_REPLACEMENT['EXPECT_TRUE_M'][op] = 'EXPECT_%s_M' % replacement
+    _CHECK_REPLACEMENT['ASSERT_TRUE_M'][op] = 'ASSERT_%s_M' % replacement
+
+for op, inv_replacement in [('==', 'NE'), ('!=', 'EQ'),
+                            ('>=', 'LT'), ('>', 'LE'),
+                            ('<=', 'GT'), ('<', 'GE')]:
+    _CHECK_REPLACEMENT['EXPECT_FALSE'][op] = 'EXPECT_%s' % inv_replacement
+    _CHECK_REPLACEMENT['ASSERT_FALSE'][op] = 'ASSERT_%s' % inv_replacement
+    _CHECK_REPLACEMENT['EXPECT_FALSE_M'][op] = 'EXPECT_%s_M' % inv_replacement
+    _CHECK_REPLACEMENT['ASSERT_FALSE_M'][op] = 'ASSERT_%s_M' % inv_replacement
+
+
+# These constants define types of headers for use with
+# _IncludeState.check_next_include_order().
+_CONFIG_HEADER = 0
+_PRIMARY_HEADER = 1
+_OTHER_HEADER = 2
+_MOC_HEADER = 3
+
+
+# A dictionary of items customize behavior for unit test. For example,
+# INCLUDE_IO_INJECTION_KEY allows providing a custom io class which allows
+# for faking a header file.
+_unit_test_config = {}
+
+
+# The regexp compilation caching is inlined in all regexp functions for
+# performance reasons; factoring it out into a separate function turns out
+# to be noticeably expensive.
+_regexp_compile_cache = {}
+
+
+def match(pattern, s):
+    """Matches the string with the pattern, caching the compiled regexp."""
+    if not pattern in _regexp_compile_cache:
+        _regexp_compile_cache[pattern] = sre_compile.compile(pattern)
+    return _regexp_compile_cache[pattern].match(s)
+
+
+def search(pattern, s):
+    """Searches the string for the pattern, caching the compiled regexp."""
+    if not pattern in _regexp_compile_cache:
+        _regexp_compile_cache[pattern] = sre_compile.compile(pattern)
+    return _regexp_compile_cache[pattern].search(s)
+
+
+def sub(pattern, replacement, s):
+    """Substitutes occurrences of a pattern, caching the compiled regexp."""
+    if not pattern in _regexp_compile_cache:
+        _regexp_compile_cache[pattern] = sre_compile.compile(pattern)
+    return _regexp_compile_cache[pattern].sub(replacement, s)
+
+
+def subn(pattern, replacement, s):
+    """Substitutes occurrences of a pattern, caching the compiled regexp."""
+    if not pattern in _regexp_compile_cache:
+        _regexp_compile_cache[pattern] = sre_compile.compile(pattern)
+    return _regexp_compile_cache[pattern].subn(replacement, s)
+
+
+def iteratively_replace_matches_with_char(pattern, char_replacement, s):
+    """Returns the string with replacement done.
+
+    Every character in the match is replaced with char.
+    Due to the iterative nature, pattern should not match char or
+    there will be an infinite loop.
+
+    Example:
+      pattern = r'<[^>]>' # template parameters
+      char_replacement =  '_'
+      s =     'A<B<C, D>>'
+      Returns 'A_________'
+
+    Args:
+      pattern: The regex to match.
+      char_replacement: The character to put in place of every
+                        character of the match.
+      s: The string on which to do the replacements.
+
+    Returns:
+      True, if the given line is blank.
+    """
+    while True:
+        matched = search(pattern, s)
+        if not matched:
+            return s
+        start_match_index = matched.start(0)
+        end_match_index = matched.end(0)
+        match_length = end_match_index - start_match_index
+        s = s[:start_match_index] + char_replacement * match_length + s[end_match_index:]
+
+
+def _rfind_in_lines(regex, lines, start_position, not_found_position):
+    """Does a reverse find starting at start position and going backwards until
+    a match is found.
+
+    Returns the position where the regex ended.
+    """
+    # Put the regex in a group and proceed it with a greedy expression that
+    # matches anything to ensure that we get the last possible match in a line.
+    last_in_line_regex = r'.*(' + regex + ')'
+    current_row = start_position.row
+
+    # Start with the given row and trim off everything past what may be matched.
+    current_line = lines[start_position.row][:start_position.column]
+    while True:
+        found_match = match(last_in_line_regex, current_line)
+        if found_match:
+            return Position(current_row, found_match.end(1))
+
+        # A match was not found so continue backward.
+        current_row -= 1
+        if current_row < 0:
+            return not_found_position
+        current_line = lines[current_row]
+
+
+def _convert_to_lower_with_underscores(text):
+    """Converts all text strings in camelCase or PascalCase to lowers with underscores."""
+
+    # First add underscores before any capital letter followed by a lower case letter
+    # as long as it is in a word.
+    # (This put an underscore before Password but not P and A in WPAPassword).
+    text = sub(r'(?<=[A-Za-z0-9])([A-Z])(?=[a-z])', r'_\1', text)
+
+    # Next add underscores before capitals at the end of words if it was
+    # preceeded by lower case letter or number.
+    # (This puts an underscore before A in isA but not A in CBA).
+    text = sub(r'(?<=[a-z0-9])([A-Z])(?=\b)', r'_\1', text)
+
+    # Next add underscores when you have a captial letter which is followed by a capital letter
+    # but is not proceeded by one. (This puts an underscore before A in 'WordADay').
+    text = sub(r'(?<=[a-z0-9])([A-Z][A-Z_])', r'_\1', text)
+
+    return text.lower()
+
+
+
+def _create_acronym(text):
+    """Creates an acronym for the given text."""
+    # Removes all lower case letters except those starting words.
+    text = sub(r'(?<!\b)[a-z]', '', text)
+    return text.upper()
+
+
+def up_to_unmatched_closing_paren(s):
+    """Splits a string into two parts up to first unmatched ')'.
+
+    Args:
+      s: a string which is a substring of line after '('
+      (e.g., "a == (b + c))").
+
+    Returns:
+      A pair of strings (prefix before first unmatched ')',
+      remainder of s after first unmatched ')'), e.g.,
+      up_to_unmatched_closing_paren("a == (b + c)) { ")
+      returns "a == (b + c)", " {".
+      Returns None, None if there is no unmatched ')'
+
+    """
+    i = 1
+    for pos, c in enumerate(s):
+      if c == '(':
+        i += 1
+      elif c == ')':
+        i -= 1
+        if i == 0:
+          return s[:pos], s[pos + 1:]
+    return None, None
+
+class _IncludeState(dict):
+    """Tracks line numbers for includes, and the order in which includes appear.
+
+    As a dict, an _IncludeState object serves as a mapping between include
+    filename and line number on which that file was included.
+
+    Call check_next_include_order() once for each header in the file, passing
+    in the type constants defined above. Calls in an illegal order will
+    raise an _IncludeError with an appropriate error message.
+
+    """
+    # self._section will move monotonically through this set. If it ever
+    # needs to move backwards, check_next_include_order will raise an error.
+    _INITIAL_SECTION = 0
+    _CONFIG_SECTION = 1
+    _PRIMARY_SECTION = 2
+    _OTHER_SECTION = 3
+
+    _TYPE_NAMES = {
+        _CONFIG_HEADER: 'WebCore config.h',
+        _PRIMARY_HEADER: 'header this file implements',
+        _OTHER_HEADER: 'other header',
+        _MOC_HEADER: 'moc file',
+        }
+    _SECTION_NAMES = {
+        _INITIAL_SECTION: "... nothing.",
+        _CONFIG_SECTION: "WebCore config.h.",
+        _PRIMARY_SECTION: 'a header this file implements.',
+        _OTHER_SECTION: 'other header.',
+        }
+
+    def __init__(self):
+        dict.__init__(self)
+        self._section = self._INITIAL_SECTION
+        self._visited_primary_section = False
+        self.header_types = dict();
+
+    def visited_primary_section(self):
+        return self._visited_primary_section
+
+    def check_next_include_order(self, header_type, file_is_header, primary_header_exists):
+        """Returns a non-empty error message if the next header is out of order.
+
+        This function also updates the internal state to be ready to check
+        the next include.
+
+        Args:
+          header_type: One of the _XXX_HEADER constants defined above.
+          file_is_header: Whether the file that owns this _IncludeState is itself a header
+
+        Returns:
+          The empty string if the header is in the right order, or an
+          error message describing what's wrong.
+
+        """
+        if header_type == _CONFIG_HEADER and file_is_header:
+            return 'Header file should not contain WebCore config.h.'
+        if header_type == _PRIMARY_HEADER and file_is_header:
+            return 'Header file should not contain itself.'
+        if header_type == _MOC_HEADER:
+            return ''
+
+        error_message = ''
+        if self._section != self._OTHER_SECTION:
+            before_error_message = ('Found %s before %s' %
+                                    (self._TYPE_NAMES[header_type],
+                                     self._SECTION_NAMES[self._section + 1]))
+        after_error_message = ('Found %s after %s' %
+                                (self._TYPE_NAMES[header_type],
+                                 self._SECTION_NAMES[self._section]))
+
+        if header_type == _CONFIG_HEADER:
+            if self._section >= self._CONFIG_SECTION:
+                error_message = after_error_message
+            self._section = self._CONFIG_SECTION
+        elif header_type == _PRIMARY_HEADER:
+            if self._section >= self._PRIMARY_SECTION:
+                error_message = after_error_message
+            elif self._section < self._CONFIG_SECTION:
+                error_message = before_error_message
+            self._section = self._PRIMARY_SECTION
+            self._visited_primary_section = True
+        else:
+            assert header_type == _OTHER_HEADER
+            if not file_is_header and self._section < self._PRIMARY_SECTION:
+                if primary_header_exists:
+                    error_message = before_error_message
+            self._section = self._OTHER_SECTION
+
+        return error_message
+
+
+class Position(object):
+    """Holds the position of something."""
+    def __init__(self, row, column):
+        self.row = row
+        self.column = column
+
+    def __str__(self):
+        return '(%s, %s)' % (self.row, self.column)
+
+    def __cmp__(self, other):
+        return self.row.__cmp__(other.row) or self.column.__cmp__(other.column)
+
+
+class Parameter(object):
+    """Information about one function parameter."""
+    def __init__(self, parameter, parameter_name_index, row):
+        self.type = parameter[:parameter_name_index].strip()
+        # Remove any initializers from the parameter name (e.g. int i = 5).
+        self.name = sub(r'=.*', '', parameter[parameter_name_index:]).strip()
+        self.row = row
+
+    @memoized
+    def lower_with_underscores_name(self):
+        """Returns the parameter name in the lower with underscores format."""
+        return _convert_to_lower_with_underscores(self.name)
+
+
+class SingleLineView(object):
+    """Converts multiple lines into a single line (with line breaks replaced by a
+       space) to allow for easier searching."""
+    def __init__(self, lines, start_position, end_position):
+        """Create a SingleLineView instance.
+
+        Args:
+          lines: a list of multiple lines to combine into a single line.
+          start_position: offset within lines of where to start the single line.
+          end_position: just after where to end (like a slice operation).
+        """
+        # Get the rows of interest.
+        trimmed_lines = lines[start_position.row:end_position.row + 1]
+
+        # Remove the columns on the last line that aren't included.
+        trimmed_lines[-1] = trimmed_lines[-1][:end_position.column]
+
+        # Remove the columns on the first line that aren't included.
+        trimmed_lines[0] = trimmed_lines[0][start_position.column:]
+
+        # Create a single line with all of the parameters.
+        self.single_line = ' '.join(trimmed_lines)
+
+        # Keep the row lengths, so we can calculate the original row number
+        # given a column in the single line (adding 1 due to the space added
+        # during the join).
+        self._row_lengths = [len(line) + 1 for line in trimmed_lines]
+        self._starting_row = start_position.row
+
+    def convert_column_to_row(self, single_line_column_number):
+        """Convert the column number from the single line into the original
+        line number.
+
+        Special cases:
+        * Columns in the added spaces are considered part of the previous line.
+        * Columns beyond the end of the line are consider part the last line
+        in the view."""
+        total_columns = 0
+        row_offset = 0
+        while row_offset < len(self._row_lengths) - 1 and single_line_column_number >= total_columns + self._row_lengths[row_offset]:
+            total_columns += self._row_lengths[row_offset]
+            row_offset += 1
+        return self._starting_row + row_offset
+
+
+def create_skeleton_parameters(all_parameters):
+    """Converts a parameter list to a skeleton version.
+
+    The skeleton only has one word for the parameter name, one word for the type,
+    and commas after each parameter and only there. Everything in the skeleton
+    remains in the same columns as the original."""
+    all_simplifications = (
+        # Remove template parameters, function declaration parameters, etc.
+        r'(<[^<>]*?>)|(\([^\(\)]*?\))|(\{[^\{\}]*?\})',
+        # Remove all initializers.
+        r'=[^,]*',
+        # Remove :: and everything before it.
+        r'[^,]*::',
+        # Remove modifiers like &, *.
+        r'[&*]',
+        # Remove const modifiers.
+        r'\bconst\s+(?=[A-Za-z])',
+        # Remove numerical modifiers like long.
+        r'\b(unsigned|long|short)\s+(?=unsigned|long|short|int|char|double|float)')
+
+    skeleton_parameters = all_parameters
+    for simplification in all_simplifications:
+        skeleton_parameters = iteratively_replace_matches_with_char(simplification, ' ', skeleton_parameters)
+    # If there are any parameters, then add a , after the last one to
+    # make a regular pattern of a , following every parameter.
+    if skeleton_parameters.strip():
+        skeleton_parameters += ','
+    return skeleton_parameters
+
+
+def find_parameter_name_index(skeleton_parameter):
+    """Determines where the parametere name starts given the skeleton parameter."""
+    # The first space from the right in the simplified parameter is where the parameter
+    # name starts unless the first space is before any content in the simplified parameter.
+    before_name_index = skeleton_parameter.rstrip().rfind(' ')
+    if before_name_index != -1 and skeleton_parameter[:before_name_index].strip():
+        return before_name_index + 1
+    return len(skeleton_parameter)
+
+
+def parameter_list(elided_lines, start_position, end_position):
+    """Generator for a function's parameters."""
+    # Create new positions that omit the outer parenthesis of the parameters.
+    start_position = Position(row=start_position.row, column=start_position.column + 1)
+    end_position = Position(row=end_position.row, column=end_position.column - 1)
+    single_line_view = SingleLineView(elided_lines, start_position, end_position)
+    skeleton_parameters = create_skeleton_parameters(single_line_view.single_line)
+    end_index = -1
+
+    while True:
+        # Find the end of the next parameter.
+        start_index = end_index + 1
+        end_index = skeleton_parameters.find(',', start_index)
+
+        # No comma means that all parameters have been parsed.
+        if end_index == -1:
+            return
+        row = single_line_view.convert_column_to_row(end_index)
+
+        # Parse the parameter into a type and parameter name.
+        skeleton_parameter = skeleton_parameters[start_index:end_index]
+        name_offset = find_parameter_name_index(skeleton_parameter)
+        parameter = single_line_view.single_line[start_index:end_index]
+        yield Parameter(parameter, name_offset, row)
+
+
+class _FunctionState(object):
+    """Tracks current function name and the number of lines in its body.
+
+    Attributes:
+      min_confidence: The minimum confidence level to use while checking style.
+
+    """
+
+    _NORMAL_TRIGGER = 250  # for --v=0, 500 for --v=1, etc.
+    _TEST_TRIGGER = 400    # about 50% more than _NORMAL_TRIGGER.
+
+    def __init__(self, min_confidence):
+        self.min_confidence = min_confidence
+        self.current_function = ''
+        self.in_a_function = False
+        self.lines_in_function = 0
+        # Make sure these will not be mistaken for real positions (even when a
+        # small amount is added to them).
+        self.body_start_position = Position(-1000, 0)
+        self.end_position = Position(-1000, 0)
+
+    def begin(self, function_name, function_name_start_position, body_start_position, end_position,
+              parameter_start_position, parameter_end_position, clean_lines):
+        """Start analyzing function body.
+
+        Args:
+            function_name: The name of the function being tracked.
+            function_name_start_position: Position in elided where the function name starts.
+            body_start_position: Position in elided of the { or the ; for a prototype.
+            end_position: Position in elided just after the final } (or ; is.
+            parameter_start_position: Position in elided of the '(' for the parameters.
+            parameter_end_position: Position in elided just after the ')' for the parameters.
+            clean_lines: A CleansedLines instance containing the file.
+        """
+        self.in_a_function = True
+        self.lines_in_function = -1  # Don't count the open brace line.
+        self.current_function = function_name
+        self.function_name_start_position = function_name_start_position
+        self.body_start_position = body_start_position
+        self.end_position = end_position
+        self.is_declaration = clean_lines.elided[body_start_position.row][body_start_position.column] == ';'
+        self.parameter_start_position = parameter_start_position
+        self.parameter_end_position = parameter_end_position
+        self.is_pure = False
+        if self.is_declaration:
+            characters_after_parameters = SingleLineView(clean_lines.elided, parameter_end_position, body_start_position).single_line
+            self.is_pure = bool(match(r'\s*=\s*0\s*', characters_after_parameters))
+        self._clean_lines = clean_lines
+        self._parameter_list = None
+
+    def modifiers_and_return_type(self):
+        """Returns the modifiers and the return type."""
+        # Go backwards from where the function name is until we encounter one of several things:
+        #   ';' or '{' or '}' or 'private:', etc. or '#' or return Position(0, 0)
+        elided = self._clean_lines.elided
+        start_modifiers = _rfind_in_lines(r';|\{|\}|((private|public|protected):)|(#.*)',
+                                          elided, self.parameter_start_position, Position(0, 0))
+        return SingleLineView(elided, start_modifiers, self.function_name_start_position).single_line.strip()
+
+    def parameter_list(self):
+        if not self._parameter_list:
+            # Store the final result as a tuple since that is immutable.
+            self._parameter_list = tuple(parameter_list(self._clean_lines.elided, self.parameter_start_position, self.parameter_end_position))
+
+        return self._parameter_list
+
+    def count(self, line_number):
+        """Count line in current function body."""
+        if self.in_a_function and line_number >= self.body_start_position.row:
+            self.lines_in_function += 1
+
+    def check(self, error, line_number):
+        """Report if too many lines in function body.
+
+        Args:
+          error: The function to call with any errors found.
+          line_number: The number of the line to check.
+        """
+        if match(r'T(EST|est)', self.current_function):
+            base_trigger = self._TEST_TRIGGER
+        else:
+            base_trigger = self._NORMAL_TRIGGER
+        trigger = base_trigger * 2 ** self.min_confidence
+
+        if self.lines_in_function > trigger:
+            error_level = int(math.log(self.lines_in_function / base_trigger, 2))
+            # 50 => 0, 100 => 1, 200 => 2, 400 => 3, 800 => 4, 1600 => 5, ...
+            if error_level > 5:
+                error_level = 5
+            error(line_number, 'readability/fn_size', error_level,
+                  'Small and focused functions are preferred:'
+                  ' %s has %d non-comment lines'
+                  ' (error triggered by exceeding %d lines).'  % (
+                      self.current_function, self.lines_in_function, trigger))
+
+    def end(self):
+        """Stop analyzing function body."""
+        self.in_a_function = False
+
+
+class _IncludeError(Exception):
+    """Indicates a problem with the include order in a file."""
+    pass
+
+
+class FileInfo:
+    """Provides utility functions for filenames.
+
+    FileInfo provides easy access to the components of a file's path
+    relative to the project root.
+    """
+
+    def __init__(self, filename):
+        self._filename = filename
+
+    def full_name(self):
+        """Make Windows paths like Unix."""
+        return os.path.abspath(self._filename).replace('\\', '/')
+
+    def repository_name(self):
+        """Full name after removing the local path to the repository.
+
+        If we have a real absolute path name here we can try to do something smart:
+        detecting the root of the checkout and truncating /path/to/checkout from
+        the name so that we get header guards that don't include things like
+        "C:\Documents and Settings\..." or "/home/username/..." in them and thus
+        people on different computers who have checked the source out to different
+        locations won't see bogus errors.
+        """
+        fullname = self.full_name()
+
+        if os.path.exists(fullname):
+            project_dir = os.path.dirname(fullname)
+
+            if os.path.exists(os.path.join(project_dir, ".svn")):
+                # If there's a .svn file in the current directory, we
+                # recursively look up the directory tree for the top
+                # of the SVN checkout
+                root_dir = project_dir
+                one_up_dir = os.path.dirname(root_dir)
+                while os.path.exists(os.path.join(one_up_dir, ".svn")):
+                    root_dir = os.path.dirname(root_dir)
+                    one_up_dir = os.path.dirname(one_up_dir)
+
+                prefix = os.path.commonprefix([root_dir, project_dir])
+                return fullname[len(prefix) + 1:]
+
+            # Not SVN? Try to find a git top level directory by
+            # searching up from the current path.
+            root_dir = os.path.dirname(fullname)
+            while (root_dir != os.path.dirname(root_dir)
+                   and not os.path.exists(os.path.join(root_dir, ".git"))):
+                root_dir = os.path.dirname(root_dir)
+                if os.path.exists(os.path.join(root_dir, ".git")):
+                    prefix = os.path.commonprefix([root_dir, project_dir])
+                    return fullname[len(prefix) + 1:]
+
+        # Don't know what to do; header guard warnings may be wrong...
+        return fullname
+
+    def split(self):
+        """Splits the file into the directory, basename, and extension.
+
+        For 'chrome/browser/browser.cpp', Split() would
+        return ('chrome/browser', 'browser', '.cpp')
+
+        Returns:
+          A tuple of (directory, basename, extension).
+        """
+
+        googlename = self.repository_name()
+        project, rest = os.path.split(googlename)
+        return (project,) + os.path.splitext(rest)
+
+    def base_name(self):
+        """File base name - text after the final slash, before the final period."""
+        return self.split()[1]
+
+    def extension(self):
+        """File extension - text following the final period."""
+        return self.split()[2]
+
+    def no_extension(self):
+        """File has no source file extension."""
+        return '/'.join(self.split()[0:2])
+
+    def is_source(self):
+        """File has a source file extension."""
+        return self.extension()[1:] in ('c', 'cc', 'cpp', 'cxx')
+
+
+# Matches standard C++ escape esequences per 2.13.2.3 of the C++ standard.
+_RE_PATTERN_CLEANSE_LINE_ESCAPES = re.compile(
+    r'\\([abfnrtv?"\\\']|\d+|x[0-9a-fA-F]+)')
+# Matches strings.  Escape codes should already be removed by ESCAPES.
+_RE_PATTERN_CLEANSE_LINE_DOUBLE_QUOTES = re.compile(r'"[^"]*"')
+# Matches characters.  Escape codes should already be removed by ESCAPES.
+_RE_PATTERN_CLEANSE_LINE_SINGLE_QUOTES = re.compile(r"'.'")
+# Matches multi-line C++ comments.
+# This RE is a little bit more complicated than one might expect, because we
+# have to take care of space removals tools so we can handle comments inside
+# statements better.
+# The current rule is: We only clear spaces from both sides when we're at the
+# end of the line. Otherwise, we try to remove spaces from the right side,
+# if this doesn't work we try on left side but only if there's a non-character
+# on the right.
+_RE_PATTERN_CLEANSE_LINE_C_COMMENTS = re.compile(
+    r"""(\s*/\*.*\*/\s*$|
+            /\*.*\*/\s+|
+         \s+/\*.*\*/(?=\W)|
+            /\*.*\*/)""", re.VERBOSE)
+
+
+def is_cpp_string(line):
+    """Does line terminate so, that the next symbol is in string constant.
+
+    This function does not consider single-line nor multi-line comments.
+
+    Args:
+      line: is a partial line of code starting from the 0..n.
+
+    Returns:
+      True, if next character appended to 'line' is inside a
+      string constant.
+    """
+
+    line = line.replace(r'\\', 'XX')  # after this, \\" does not match to \"
+    return ((line.count('"') - line.count(r'\"') - line.count("'\"'")) & 1) == 1
+
+
+def find_next_multi_line_comment_start(lines, line_index):
+    """Find the beginning marker for a multiline comment."""
+    while line_index < len(lines):
+        if lines[line_index].strip().startswith('/*'):
+            # Only return this marker if the comment goes beyond this line
+            if lines[line_index].strip().find('*/', 2) < 0:
+                return line_index
+        line_index += 1
+    return len(lines)
+
+
+def find_next_multi_line_comment_end(lines, line_index):
+    """We are inside a comment, find the end marker."""
+    while line_index < len(lines):
+        if lines[line_index].strip().endswith('*/'):
+            return line_index
+        line_index += 1
+    return len(lines)
+
+
+def remove_multi_line_comments_from_range(lines, begin, end):
+    """Clears a range of lines for multi-line comments."""
+    # Having // dummy comments makes the lines non-empty, so we will not get
+    # unnecessary blank line warnings later in the code.
+    for i in range(begin, end):
+        lines[i] = '// dummy'
+
+
+def remove_multi_line_comments(lines, error):
+    """Removes multiline (c-style) comments from lines."""
+    line_index = 0
+    while line_index < len(lines):
+        line_index_begin = find_next_multi_line_comment_start(lines, line_index)
+        if line_index_begin >= len(lines):
+            return
+        line_index_end = find_next_multi_line_comment_end(lines, line_index_begin)
+        if line_index_end >= len(lines):
+            error(line_index_begin + 1, 'readability/multiline_comment', 5,
+                  'Could not find end of multi-line comment')
+            return
+        remove_multi_line_comments_from_range(lines, line_index_begin, line_index_end + 1)
+        line_index = line_index_end + 1
+
+
+def cleanse_comments(line):
+    """Removes //-comments and single-line C-style /* */ comments.
+
+    Args:
+      line: A line of C++ source.
+
+    Returns:
+      The line with single-line comments removed.
+    """
+    comment_position = line.find('//')
+    if comment_position != -1 and not is_cpp_string(line[:comment_position]):
+        line = line[:comment_position]
+    # get rid of /* ... */
+    return _RE_PATTERN_CLEANSE_LINE_C_COMMENTS.sub('', line)
+
+
+class CleansedLines(object):
+    """Holds 3 copies of all lines with different preprocessing applied to them.
+
+    1) elided member contains lines without strings and comments,
+    2) lines member contains lines without comments, and
+    3) raw member contains all the lines without processing.
+    All these three members are of <type 'list'>, and of the same length.
+    """
+
+    def __init__(self, lines):
+        self.elided = []
+        self.lines = []
+        self.raw_lines = lines
+        self._num_lines = len(lines)
+        for line_number in range(len(lines)):
+            self.lines.append(cleanse_comments(lines[line_number]))
+            elided = self.collapse_strings(lines[line_number])
+            self.elided.append(cleanse_comments(elided))
+
+    def num_lines(self):
+        """Returns the number of lines represented."""
+        return self._num_lines
+
+    @staticmethod
+    def collapse_strings(elided):
+        """Collapses strings and chars on a line to simple "" or '' blocks.
+
+        We nix strings first so we're not fooled by text like '"http://"'
+
+        Args:
+          elided: The line being processed.
+
+        Returns:
+          The line with collapsed strings.
+        """
+        if not _RE_PATTERN_INCLUDE.match(elided):
+            # Remove escaped characters first to make quote/single quote collapsing
+            # basic.  Things that look like escaped characters shouldn't occur
+            # outside of strings and chars.
+            elided = _RE_PATTERN_CLEANSE_LINE_ESCAPES.sub('', elided)
+            elided = _RE_PATTERN_CLEANSE_LINE_SINGLE_QUOTES.sub("''", elided)
+            elided = _RE_PATTERN_CLEANSE_LINE_DOUBLE_QUOTES.sub('""', elided)
+        return elided
+
+
+def close_expression(elided, position):
+    """If input points to ( or { or [, finds the position that closes it.
+
+    If elided[position.row][position.column] points to a '(' or '{' or '[',
+    finds the line_number/pos that correspond to the closing of the expression.
+
+     Args:
+       elided: A CleansedLines.elided instance containing the file.
+       position: The position of the opening item.
+
+     Returns:
+      The Position *past* the closing brace, or Position(len(elided), -1)
+      if we never find a close. Note we ignore strings and comments when matching.
+    """
+    line = elided[position.row]
+    start_character = line[position.column]
+    if start_character == '(':
+        enclosing_character_regex = r'[\(\)]'
+    elif start_character == '[':
+        enclosing_character_regex = r'[\[\]]'
+    elif start_character == '{':
+        enclosing_character_regex = r'[\{\}]'
+    else:
+        return Position(len(elided), -1)
+
+    current_column = position.column + 1
+    line_number = position.row
+    net_open = 1
+    for line in elided[position.row:]:
+        line = line[current_column:]
+
+        # Search the current line for opening and closing characters.
+        while True:
+            next_enclosing_character = search(enclosing_character_regex, line)
+            # No more on this line.
+            if not next_enclosing_character:
+                break
+            current_column += next_enclosing_character.end(0)
+            line = line[next_enclosing_character.end(0):]
+            if next_enclosing_character.group(0) == start_character:
+                net_open += 1
+            else:
+                net_open -= 1
+                if not net_open:
+                    return Position(line_number, current_column)
+
+        # Proceed to the next line.
+        line_number += 1
+        current_column = 0
+
+    # The given item was not closed.
+    return Position(len(elided), -1)
+
+def check_for_copyright(lines, error):
+    """Logs an error if no Copyright message appears at the top of the file."""
+
+    # We'll say it should occur by line 10. Don't forget there's a
+    # dummy line at the front.
+    for line in xrange(1, min(len(lines), 11)):
+        if re.search(r'Copyright', lines[line], re.I):
+            break
+    else:                       # means no copyright line was found
+        error(0, 'legal/copyright', 5,
+              'No copyright message found.  '
+              'You should have a line: "Copyright [year] <Copyright Owner>"')
+
+
+def get_header_guard_cpp_variable(filename):
+    """Returns the CPP variable that should be used as a header guard.
+
+    Args:
+      filename: The name of a C++ header file.
+
+    Returns:
+      The CPP variable that should be used as a header guard in the
+      named file.
+
+    """
+
+    # Restores original filename in case that style checker is invoked from Emacs's
+    # flymake.
+    filename = re.sub(r'_flymake\.h$', '.h', filename)
+
+    standard_name = sub(r'[-.\s]', '_', os.path.basename(filename))
+
+    # Files under WTF typically have header guards that start with WTF_.
+    if '/wtf/' in filename:
+        special_name = "WTF_" + standard_name
+    else:
+        special_name = standard_name
+    return (special_name, standard_name)
+
+
+def check_for_header_guard(filename, lines, error):
+    """Checks that the file contains a header guard.
+
+    Logs an error if no #ifndef header guard is present.  For other
+    headers, checks that the full pathname is used.
+
+    Args:
+      filename: The name of the C++ header file.
+      lines: An array of strings, each representing a line of the file.
+      error: The function to call with any errors found.
+    """
+
+    cppvar = get_header_guard_cpp_variable(filename)
+
+    ifndef = None
+    ifndef_line_number = 0
+    define = None
+    for line_number, line in enumerate(lines):
+        line_split = line.split()
+        if len(line_split) >= 2:
+            # find the first occurrence of #ifndef and #define, save arg
+            if not ifndef and line_split[0] == '#ifndef':
+                # set ifndef to the header guard presented on the #ifndef line.
+                ifndef = line_split[1]
+                ifndef_line_number = line_number
+            if not define and line_split[0] == '#define':
+                define = line_split[1]
+            if define and ifndef:
+                break
+
+    if not ifndef or not define or ifndef != define:
+        error(0, 'build/header_guard', 5,
+              'No #ifndef header guard found, suggested CPP variable is: %s' %
+              cppvar[0])
+        return
+
+    # The guard should be File_h.
+    if ifndef not in cppvar:
+        error(ifndef_line_number, 'build/header_guard', 5,
+              '#ifndef header guard has wrong style, please use: %s' % cppvar[0])
+
+
+def check_for_unicode_replacement_characters(lines, error):
+    """Logs an error for each line containing Unicode replacement characters.
+
+    These indicate that either the file contained invalid UTF-8 (likely)
+    or Unicode replacement characters (which it shouldn't).  Note that
+    it's possible for this to throw off line numbering if the invalid
+    UTF-8 occurred adjacent to a newline.
+
+    Args:
+      lines: An array of strings, each representing a line of the file.
+      error: The function to call with any errors found.
+    """
+    for line_number, line in enumerate(lines):
+        if u'\ufffd' in line:
+            error(line_number, 'readability/utf8', 5,
+                  'Line contains invalid UTF-8 (or Unicode replacement character).')
+
+
+def check_for_new_line_at_eof(lines, error):
+    """Logs an error if there is no newline char at the end of the file.
+
+    Args:
+      lines: An array of strings, each representing a line of the file.
+      error: The function to call with any errors found.
+    """
+
+    # The array lines() was created by adding two newlines to the
+    # original file (go figure), then splitting on \n.
+    # To verify that the file ends in \n, we just have to make sure the
+    # last-but-two element of lines() exists and is empty.
+    if len(lines) < 3 or lines[-2]:
+        error(len(lines) - 2, 'whitespace/ending_newline', 5,
+              'Could not find a newline character at the end of the file.')
+
+
+def check_for_multiline_comments_and_strings(clean_lines, line_number, error):
+    """Logs an error if we see /* ... */ or "..." that extend past one line.
+
+    /* ... */ comments are legit inside macros, for one line.
+    Otherwise, we prefer // comments, so it's ok to warn about the
+    other.  Likewise, it's ok for strings to extend across multiple
+    lines, as long as a line continuation character (backslash)
+    terminates each line. Although not currently prohibited by the C++
+    style guide, it's ugly and unnecessary. We don't do well with either
+    in this lint program, so we warn about both.
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      error: The function to call with any errors found.
+    """
+    line = clean_lines.elided[line_number]
+
+    # Remove all \\ (escaped backslashes) from the line. They are OK, and the
+    # second (escaped) slash may trigger later \" detection erroneously.
+    line = line.replace('\\\\', '')
+
+    if line.count('/*') > line.count('*/'):
+        error(line_number, 'readability/multiline_comment', 5,
+              'Complex multi-line /*...*/-style comment found. '
+              'Lint may give bogus warnings.  '
+              'Consider replacing these with //-style comments, '
+              'with #if 0...#endif, '
+              'or with more clearly structured multi-line comments.')
+
+    if (line.count('"') - line.count('\\"')) % 2:
+        error(line_number, 'readability/multiline_string', 5,
+              'Multi-line string ("...") found.  This lint script doesn\'t '
+              'do well with such strings, and may give bogus warnings.  They\'re '
+              'ugly and unnecessary, and you should use concatenation instead".')
+
+
+_THREADING_LIST = (
+    ('asctime(', 'asctime_r('),
+    ('ctime(', 'ctime_r('),
+    ('getgrgid(', 'getgrgid_r('),
+    ('getgrnam(', 'getgrnam_r('),
+    ('getlogin(', 'getlogin_r('),
+    ('getpwnam(', 'getpwnam_r('),
+    ('getpwuid(', 'getpwuid_r('),
+    ('gmtime(', 'gmtime_r('),
+    ('localtime(', 'localtime_r('),
+    ('rand(', 'rand_r('),
+    ('readdir(', 'readdir_r('),
+    ('strtok(', 'strtok_r('),
+    ('ttyname(', 'ttyname_r('),
+    )
+
+
+def check_posix_threading(clean_lines, line_number, error):
+    """Checks for calls to thread-unsafe functions.
+
+    Much code has been originally written without consideration of
+    multi-threading. Also, engineers are relying on their old experience;
+    they have learned posix before threading extensions were added. These
+    tests guide the engineers to use thread-safe functions (when using
+    posix directly).
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      error: The function to call with any errors found.
+    """
+    line = clean_lines.elided[line_number]
+    for single_thread_function, multithread_safe_function in _THREADING_LIST:
+        index = line.find(single_thread_function)
+        # Comparisons made explicit for clarity -- pylint: disable-msg=C6403
+        if index >= 0 and (index == 0 or (not line[index - 1].isalnum()
+                                          and line[index - 1] not in ('_', '.', '>'))):
+            error(line_number, 'runtime/threadsafe_fn', 2,
+                  'Consider using ' + multithread_safe_function +
+                  '...) instead of ' + single_thread_function +
+                  '...) for improved thread safety.')
+
+
+# Matches invalid increment: *count++, which moves pointer instead of
+# incrementing a value.
+_RE_PATTERN_INVALID_INCREMENT = re.compile(
+    r'^\s*\*\w+(\+\+|--);')
+
+
+def check_invalid_increment(clean_lines, line_number, error):
+    """Checks for invalid increment *count++.
+
+    For example following function:
+    void increment_counter(int* count) {
+        *count++;
+    }
+    is invalid, because it effectively does count++, moving pointer, and should
+    be replaced with ++*count, (*count)++ or *count += 1.
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      error: The function to call with any errors found.
+    """
+    line = clean_lines.elided[line_number]
+    if _RE_PATTERN_INVALID_INCREMENT.match(line):
+        error(line_number, 'runtime/invalid_increment', 5,
+              'Changing pointer instead of value (or unused value of operator*).')
+
+
+class _ClassInfo(object):
+    """Stores information about a class."""
+
+    def __init__(self, name, line_number):
+        self.name = name
+        self.line_number = line_number
+        self.seen_open_brace = False
+        self.is_derived = False
+        self.virtual_method_line_number = None
+        self.has_virtual_destructor = False
+        self.brace_depth = 0
+
+
+class _ClassState(object):
+    """Holds the current state of the parse relating to class declarations.
+
+    It maintains a stack of _ClassInfos representing the parser's guess
+    as to the current nesting of class declarations. The innermost class
+    is at the top (back) of the stack. Typically, the stack will either
+    be empty or have exactly one entry.
+    """
+
+    def __init__(self):
+        self.classinfo_stack = []
+
+    def check_finished(self, error):
+        """Checks that all classes have been completely parsed.
+
+        Call this when all lines in a file have been processed.
+        Args:
+          error: The function to call with any errors found.
+        """
+        if self.classinfo_stack:
+            # Note: This test can result in false positives if #ifdef constructs
+            # get in the way of brace matching. See the testBuildClass test in
+            # cpp_style_unittest.py for an example of this.
+            error(self.classinfo_stack[0].line_number, 'build/class', 5,
+                  'Failed to find complete declaration of class %s' %
+                  self.classinfo_stack[0].name)
+
+
+class _FileState(object):
+    def __init__(self, clean_lines, file_extension):
+        self._did_inside_namespace_indent_warning = False
+        self._clean_lines = clean_lines
+        if file_extension in ['m', 'mm']:
+            self._is_objective_c = True
+            self._is_c = False
+        elif file_extension == 'h':
+            # In the case of header files, it is unknown if the file
+            # is c / objective c or not, so set this value to None and then
+            # if it is requested, use heuristics to guess the value.
+            self._is_objective_c = None
+            self._is_c = None
+        elif file_extension == 'c':
+            self._is_c = True
+            self._is_objective_c = False
+        else:
+            self._is_objective_c = False
+            self._is_c = False
+
+    def set_did_inside_namespace_indent_warning(self):
+        self._did_inside_namespace_indent_warning = True
+
+    def did_inside_namespace_indent_warning(self):
+        return self._did_inside_namespace_indent_warning
+
+    def is_objective_c(self):
+        if self._is_objective_c is None:
+            for line in self._clean_lines.elided:
+                # Starting with @ or #import seem like the best indications
+                # that we have an Objective C file.
+                if line.startswith("@") or line.startswith("#import"):
+                    self._is_objective_c = True
+                    break
+            else:
+                self._is_objective_c = False
+        return self._is_objective_c
+
+    def is_c(self):
+        if self._is_c is None:
+            for line in self._clean_lines.lines:
+                # if extern "C" is found, then it is a good indication
+                # that we have a C header file.
+                if line.startswith('extern "C"'):
+                    self._is_c = True
+                    break
+            else:
+                self._is_c = False
+        return self._is_c
+
+    def is_c_or_objective_c(self):
+        """Return whether the file extension corresponds to C or Objective-C."""
+        return self.is_c() or self.is_objective_c()
+
+
+def check_for_non_standard_constructs(clean_lines, line_number,
+                                      class_state, error):
+    """Logs an error if we see certain non-ANSI constructs ignored by gcc-2.
+
+    Complain about several constructs which gcc-2 accepts, but which are
+    not standard C++.  Warning about these in lint is one way to ease the
+    transition to new compilers.
+    - put storage class first (e.g. "static const" instead of "const static").
+    - "%lld" instead of %qd" in printf-type functions.
+    - "%1$d" is non-standard in printf-type functions.
+    - "\%" is an undefined character escape sequence.
+    - text after #endif is not allowed.
+    - invalid inner-style forward declaration.
+    - >? and <? operators, and their >?= and <?= cousins.
+    - classes with virtual methods need virtual destructors (compiler warning
+        available, but not turned on yet.)
+
+    Additionally, check for constructor/destructor style violations as it
+    is very convenient to do so while checking for gcc-2 compliance.
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      class_state: A _ClassState instance which maintains information about
+                   the current stack of nested class declarations being parsed.
+      error: A callable to which errors are reported, which takes parameters:
+             line number, error level, and message
+    """
+
+    # Remove comments from the line, but leave in strings for now.
+    line = clean_lines.lines[line_number]
+
+    if search(r'printf\s*\(.*".*%[-+ ]?\d*q', line):
+        error(line_number, 'runtime/printf_format', 3,
+              '%q in format strings is deprecated.  Use %ll instead.')
+
+    if search(r'printf\s*\(.*".*%\d+\$', line):
+        error(line_number, 'runtime/printf_format', 2,
+              '%N$ formats are unconventional.  Try rewriting to avoid them.')
+
+    # Remove escaped backslashes before looking for undefined escapes.
+    line = line.replace('\\\\', '')
+
+    if search(r'("|\').*\\(%|\[|\(|{)', line):
+        error(line_number, 'build/printf_format', 3,
+              '%, [, (, and { are undefined character escapes.  Unescape them.')
+
+    # For the rest, work with both comments and strings removed.
+    line = clean_lines.elided[line_number]
+
+    if search(r'\b(const|volatile|void|char|short|int|long'
+              r'|float|double|signed|unsigned'
+              r'|schar|u?int8|u?int16|u?int32|u?int64)'
+              r'\s+(auto|register|static|extern|typedef)\b',
+              line):
+        error(line_number, 'build/storage_class', 5,
+              'Storage class (static, extern, typedef, etc) should be first.')
+
+    if match(r'\s*#\s*endif\s*[^/\s]+', line):
+        error(line_number, 'build/endif_comment', 5,
+              'Uncommented text after #endif is non-standard.  Use a comment.')
+
+    if match(r'\s*class\s+(\w+\s*::\s*)+\w+\s*;', line):
+        error(line_number, 'build/forward_decl', 5,
+              'Inner-style forward declarations are invalid.  Remove this line.')
+
+    if search(r'(\w+|[+-]?\d+(\.\d*)?)\s*(<|>)\?=?\s*(\w+|[+-]?\d+)(\.\d*)?', line):
+        error(line_number, 'build/deprecated', 3,
+              '>? and <? (max and min) operators are non-standard and deprecated.')
+
+    # Track class entry and exit, and attempt to find cases within the
+    # class declaration that don't meet the C++ style
+    # guidelines. Tracking is very dependent on the code matching Google
+    # style guidelines, but it seems to perform well enough in testing
+    # to be a worthwhile addition to the checks.
+    classinfo_stack = class_state.classinfo_stack
+    # Look for a class declaration
+    class_decl_match = match(
+        r'\s*(template\s*<[\w\s<>,:]*>\s*)?(class|struct)\s+(\w+(::\w+)*)', line)
+    if class_decl_match:
+        classinfo_stack.append(_ClassInfo(class_decl_match.group(3), line_number))
+
+    # Everything else in this function uses the top of the stack if it's
+    # not empty.
+    if not classinfo_stack:
+        return
+
+    classinfo = classinfo_stack[-1]
+
+    # If the opening brace hasn't been seen look for it and also
+    # parent class declarations.
+    if not classinfo.seen_open_brace:
+        # If the line has a ';' in it, assume it's a forward declaration or
+        # a single-line class declaration, which we won't process.
+        if line.find(';') != -1:
+            classinfo_stack.pop()
+            return
+        classinfo.seen_open_brace = (line.find('{') != -1)
+        # Look for a bare ':'
+        if search('(^|[^:]):($|[^:])', line):
+            classinfo.is_derived = True
+        if not classinfo.seen_open_brace:
+            return  # Everything else in this function is for after open brace
+
+    # The class may have been declared with namespace or classname qualifiers.
+    # The constructor and destructor will not have those qualifiers.
+    base_classname = classinfo.name.split('::')[-1]
+
+    # Look for single-argument constructors that aren't marked explicit.
+    # Technically a valid construct, but against style.
+    args = match(r'(?<!explicit)\s+%s\s*\(([^,()]+)\)'
+                 % re.escape(base_classname),
+                 line)
+    if (args
+        and args.group(1) != 'void'
+        and not match(r'(const\s+)?%s\s*&' % re.escape(base_classname),
+                      args.group(1).strip())):
+        error(line_number, 'runtime/explicit', 5,
+              'Single-argument constructors should be marked explicit.')
+
+    # Look for methods declared virtual.
+    if search(r'\bvirtual\b', line):
+        classinfo.virtual_method_line_number = line_number
+        # Only look for a destructor declaration on the same line. It would
+        # be extremely unlikely for the destructor declaration to occupy
+        # more than one line.
+        if search(r'~%s\s*\(' % base_classname, line):
+            classinfo.has_virtual_destructor = True
+
+    # Look for class end.
+    brace_depth = classinfo.brace_depth
+    brace_depth = brace_depth + line.count('{') - line.count('}')
+    if brace_depth <= 0:
+        classinfo = classinfo_stack.pop()
+        # Try to detect missing virtual destructor declarations.
+        # For now, only warn if a non-derived class with virtual methods lacks
+        # a virtual destructor. This is to make it less likely that people will
+        # declare derived virtual destructors without declaring the base
+        # destructor virtual.
+        if ((classinfo.virtual_method_line_number is not None)
+            and (not classinfo.has_virtual_destructor)
+            and (not classinfo.is_derived)):  # Only warn for base classes
+            error(classinfo.line_number, 'runtime/virtual', 4,
+                  'The class %s probably needs a virtual destructor due to '
+                  'having virtual method(s), one declared at line %d.'
+                  % (classinfo.name, classinfo.virtual_method_line_number))
+    else:
+        classinfo.brace_depth = brace_depth
+
+
+def check_spacing_for_function_call(line, line_number, error):
+    """Checks for the correctness of various spacing around function calls.
+
+    Args:
+      line: The text of the line to check.
+      line_number: The number of the line to check.
+      error: The function to call with any errors found.
+    """
+
+    # Since function calls often occur inside if/for/foreach/while/switch
+    # expressions - which have their own, more liberal conventions - we
+    # first see if we should be looking inside such an expression for a
+    # function call, to which we can apply more strict standards.
+    function_call = line    # if there's no control flow construct, look at whole line
+    for pattern in (r'\bif\s*\((.*)\)\s*{',
+                    r'\bfor\s*\((.*)\)\s*{',
+                    r'\bforeach\s*\((.*)\)\s*{',
+                    r'\bwhile\s*\((.*)\)\s*[{;]',
+                    r'\bswitch\s*\((.*)\)\s*{'):
+        matched = search(pattern, line)
+        if matched:
+            function_call = matched.group(1)    # look inside the parens for function calls
+            break
+
+    # Except in if/for/foreach/while/switch, there should never be space
+    # immediately inside parens (eg "f( 3, 4 )").  We make an exception
+    # for nested parens ( (a+b) + c ).  Likewise, there should never be
+    # a space before a ( when it's a function argument.  I assume it's a
+    # function argument when the char before the whitespace is legal in
+    # a function name (alnum + _) and we're not starting a macro. Also ignore
+    # pointers and references to arrays and functions coz they're too tricky:
+    # we use a very simple way to recognize these:
+    # " (something)(maybe-something)" or
+    # " (something)(maybe-something," or
+    # " (something)[something]"
+    # Note that we assume the contents of [] to be short enough that
+    # they'll never need to wrap.
+    if (  # Ignore control structures.
+        not search(r'\b(if|for|foreach|while|switch|return|new|delete)\b', function_call)
+        # Ignore pointers/references to functions.
+        and not search(r' \([^)]+\)\([^)]*(\)|,$)', function_call)
+        # Ignore pointers/references to arrays.
+        and not search(r' \([^)]+\)\[[^\]]+\]', function_call)):
+        if search(r'\w\s*\([ \t](?!\s*\\$)', function_call):      # a ( used for a fn call
+            error(line_number, 'whitespace/parens', 4,
+                  'Extra space after ( in function call')
+        elif search(r'\([ \t]+(?!(\s*\\)|\()', function_call):
+            error(line_number, 'whitespace/parens', 2,
+                  'Extra space after (')
+        if (search(r'\w\s+\(', function_call)
+            and not match(r'\s*(#|typedef)', function_call)):
+            error(line_number, 'whitespace/parens', 4,
+                  'Extra space before ( in function call')
+        # If the ) is followed only by a newline or a { + newline, assume it's
+        # part of a control statement (if/while/etc), and don't complain
+        if search(r'[^)\s]\s+\)(?!\s*$|{\s*$)', function_call):
+            error(line_number, 'whitespace/parens', 2,
+                  'Extra space before )')
+
+
+def is_blank_line(line):
+    """Returns true if the given line is blank.
+
+    We consider a line to be blank if the line is empty or consists of
+    only white spaces.
+
+    Args:
+      line: A line of a string.
+
+    Returns:
+      True, if the given line is blank.
+    """
+    return not line or line.isspace()
+
+
+def detect_functions(clean_lines, line_number, function_state, error):
+    """Finds where functions start and end.
+
+    Uses a simplistic algorithm assuming other style guidelines
+    (especially spacing) are followed.
+    Trivial bodies are unchecked, so constructors with huge initializer lists
+    may be missed.
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      function_state: Current function name and lines in body so far.
+      error: The function to call with any errors found.
+    """
+    # Are we now past the end of a function?
+    if function_state.end_position.row + 1 == line_number:
+        function_state.end()
+
+    # If we're in a function, don't try to detect a new one.
+    if function_state.in_a_function:
+        return
+
+    lines = clean_lines.lines
+    line = lines[line_number]
+    raw = clean_lines.raw_lines
+    raw_line = raw[line_number]
+
+    # Lines ending with a \ indicate a macro. Don't try to check them.
+    if raw_line.endswith('\\'):
+        return
+
+    regexp = r'\s*(\w(\w|::|\*|\&|\s|<|>|,|~|(operator\s*(/|-|=|!|\+)+))*)\('  # decls * & space::name( ...
+    match_result = match(regexp, line)
+    if not match_result:
+        return
+
+    # If the name is all caps and underscores, figure it's a macro and
+    # ignore it, unless it's TEST or TEST_F.
+    function_name = match_result.group(1).split()[-1]
+    if function_name != 'TEST' and function_name != 'TEST_F' and match(r'[A-Z_]+$', function_name):
+        return
+
+    joined_line = ''
+    for start_line_number in xrange(line_number, clean_lines.num_lines()):
+        start_line = clean_lines.elided[start_line_number]
+        joined_line += ' ' + start_line.lstrip()
+        body_match = search(r'{|;', start_line)
+        if body_match:
+            body_start_position = Position(start_line_number, body_match.start(0))
+
+            # Replace template constructs with _ so that no spaces remain in the function name,
+            # while keeping the column numbers of other characters the same as "line".
+            line_with_no_templates = iteratively_replace_matches_with_char(r'<[^<>]*>', '_', line)
+            match_function = search(r'((\w|:|<|>|,|~|(operator\s*(/|-|=|!|\+)+))*)\(', line_with_no_templates)
+            if not match_function:
+                return  # The '(' must have been inside of a template.
+
+            # Use the column numbers from the modified line to find the
+            # function name in the original line.
+            function = line[match_function.start(1):match_function.end(1)]
+            function_name_start_position = Position(line_number, match_function.start(1))
+
+            if match(r'TEST', function):    # Handle TEST... macros
+                parameter_regexp = search(r'(\(.*\))', joined_line)
+                if parameter_regexp:             # Ignore bad syntax
+                    function += parameter_regexp.group(1)
+            else:
+                function += '()'
+
+            parameter_start_position = Position(line_number, match_function.end(1))
+            parameter_end_position = close_expression(clean_lines.elided, parameter_start_position)
+            if parameter_end_position.row == len(clean_lines.elided):
+                # No end was found.
+                return
+
+            if start_line[body_start_position.column] == ';':
+                end_position = Position(body_start_position.row, body_start_position.column + 1)
+            else:
+                end_position = close_expression(clean_lines.elided, body_start_position)
+
+            # Check for nonsensical positions. (This happens in test cases which check code snippets.)
+            if parameter_end_position > body_start_position:
+                return
+
+            function_state.begin(function, function_name_start_position, body_start_position, end_position,
+                                 parameter_start_position, parameter_end_position, clean_lines)
+            return
+
+    # No body for the function (or evidence of a non-function) was found.
+    error(line_number, 'readability/fn_size', 5,
+          'Lint failed to find start of function body.')
+
+
+def check_for_function_lengths(clean_lines, line_number, function_state, error):
+    """Reports for long function bodies.
+
+    For an overview why this is done, see:
+    http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Write_Short_Functions
+
+    Blank/comment lines are not counted so as to avoid encouraging the removal
+    of vertical space and commments just to get through a lint check.
+    NOLINT *on the last line of a function* disables this check.
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      function_state: Current function name and lines in body so far.
+      error: The function to call with any errors found.
+    """
+    lines = clean_lines.lines
+    line = lines[line_number]
+    raw = clean_lines.raw_lines
+    raw_line = raw[line_number]
+
+    if function_state.end_position.row == line_number:  # last line
+        if not search(r'\bNOLINT\b', raw_line):
+            function_state.check(error, line_number)
+    elif not match(r'^\s*$', line):
+        function_state.count(line_number)  # Count non-blank/non-comment lines.
+
+
+def _check_parameter_name_against_text(parameter, text, error):
+    """Checks to see if the parameter name is contained within the text.
+
+    Return false if the check failed (i.e. an error was produced).
+    """
+
+    # Treat 'lower with underscores' as a canonical form because it is
+    # case insensitive while still retaining word breaks. (This ensures that
+    # 'elate' doesn't look like it is duplicating of 'NateLate'.)
+    canonical_parameter_name = parameter.lower_with_underscores_name()
+
+    # Appends "object" to all text to catch variables that did the same (but only
+    # do this when the parameter name is more than a single character to avoid
+    # flagging 'b' which may be an ok variable when used in an rgba function).
+    if len(canonical_parameter_name) > 1:
+        text = sub(r'(\w)\b', r'\1Object', text)
+    canonical_text = _convert_to_lower_with_underscores(text)
+
+    # Used to detect cases like ec for ExceptionCode.
+    acronym = _create_acronym(text).lower()
+    if canonical_text.find(canonical_parameter_name) != -1 or acronym.find(canonical_parameter_name) != -1:
+        error(parameter.row, 'readability/parameter_name', 5,
+              'The parameter name "%s" adds no information, so it should be removed.' % parameter.name)
+        return False
+    return True
+
+
+def check_function_definition_and_pass_ptr(type_text, row, location_description, error):
+    """Check that function definitions for use Pass*Ptr instead of *Ptr.
+
+    Args:
+       type_text: A string containing the type. (For return values, it may contain more than the type.)
+       row: The row number of the type.
+       location_description: Used to indicate where the type is. This is either 'parameter' or 'return'.
+       error: The function to call with any errors found.
+    """
+    match_ref_or_own_ptr = '(?=\W|^)(Ref|Own)Ptr(?=\W)'
+    bad_type_usage = search(match_ref_or_own_ptr, type_text)
+    if not bad_type_usage or type_text.endswith('&') or type_text.endswith('*'):
+        return
+    type_name = bad_type_usage.group(0)
+    error(row, 'readability/pass_ptr', 5,
+          'The %s type should use Pass%s instead of %s.' % (location_description, type_name, type_name))
+
+
+def check_function_definition(filename, file_extension, clean_lines, line_number, function_state, error):
+    """Check that function definitions for style issues.
+
+    Specifically, check that parameter names in declarations add information.
+
+    Args:
+       filename: Filename of the file that is being processed.
+       file_extension: The current file extension, without the leading dot.
+       clean_lines: A CleansedLines instance containing the file.
+       line_number: The number of the line to check.
+       function_state: Current function name and lines in body so far.
+       error: The function to call with any errors found.
+    """
+    if line_number != function_state.body_start_position.row:
+        return
+
+    modifiers_and_return_type = function_state.modifiers_and_return_type()
+    if filename.find('/chromium/') != -1 and search(r'\bWEBKIT_EXPORT\b', modifiers_and_return_type):
+        if filename.find('/chromium/public/') == -1 and filename.find('/chromium/tests/') == -1 and filename.find('chromium/platform') == -1:
+            error(function_state.function_name_start_position.row, 'readability/webkit_export', 5,
+                  'WEBKIT_EXPORT should only appear in the chromium public (or tests) directory.')
+        elif not file_extension == "h":
+            error(function_state.function_name_start_position.row, 'readability/webkit_export', 5,
+                  'WEBKIT_EXPORT should only be used in header files.')
+        elif not function_state.is_declaration or search(r'\binline\b', modifiers_and_return_type):
+            error(function_state.function_name_start_position.row, 'readability/webkit_export', 5,
+                  'WEBKIT_EXPORT should not be used on a function with a body.')
+        elif function_state.is_pure:
+            error(function_state.function_name_start_position.row, 'readability/webkit_export', 5,
+                  'WEBKIT_EXPORT should not be used with a pure virtual function.')
+
+    check_function_definition_and_pass_ptr(modifiers_and_return_type, function_state.function_name_start_position.row, 'return', error)
+
+    parameter_list = function_state.parameter_list()
+    for parameter in parameter_list:
+        check_function_definition_and_pass_ptr(parameter.type, parameter.row, 'parameter', error)
+
+        # Do checks specific to function declarations and parameter names.
+        if not function_state.is_declaration or not parameter.name:
+            continue
+
+        # Check the parameter name against the function name for single parameter set functions.
+        if len(parameter_list) == 1 and match('set[A-Z]', function_state.current_function):
+            trimmed_function_name = function_state.current_function[len('set'):]
+            if not _check_parameter_name_against_text(parameter, trimmed_function_name, error):
+                continue  # Since an error was noted for this name, move to the next parameter.
+
+        # Check the parameter name against the type.
+        if not _check_parameter_name_against_text(parameter, parameter.type, error):
+            continue  # Since an error was noted for this name, move to the next parameter.
+
+
+def check_pass_ptr_usage(clean_lines, line_number, function_state, error):
+    """Check for proper usage of Pass*Ptr.
+
+    Currently this is limited to detecting declarations of Pass*Ptr
+    variables inside of functions.
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      function_state: Current function name and lines in body so far.
+      error: The function to call with any errors found.
+    """
+    if not function_state.in_a_function:
+        return
+
+    lines = clean_lines.lines
+    line = lines[line_number]
+    if line_number > function_state.body_start_position.row:
+        matched_pass_ptr = match(r'^\s*Pass([A-Z][A-Za-z]*)Ptr<', line)
+        if matched_pass_ptr:
+            type_name = 'Pass%sPtr' % matched_pass_ptr.group(1)
+            error(line_number, 'readability/pass_ptr', 5,
+                  'Local variables should never be %s (see '
+                  'http://webkit.org/coding/RefPtr.html).' % type_name)
+
+
+def check_for_leaky_patterns(clean_lines, line_number, function_state, error):
+    """Check for constructs known to be leak prone.
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      function_state: Current function name and lines in body so far.
+      error: The function to call with any errors found.
+    """
+    lines = clean_lines.lines
+    line = lines[line_number]
+
+    matched_get_dc = search(r'\b(?P<function_name>GetDC(Ex)?)\s*\(', line)
+    if matched_get_dc:
+        error(line_number, 'runtime/leaky_pattern', 5,
+              'Use the class HWndDC instead of calling %s to avoid potential '
+              'memory leaks.' % matched_get_dc.group('function_name'))
+
+    matched_create_dc = search(r'\b(?P<function_name>Create(Compatible)?DC)\s*\(', line)
+    matched_own_dc = search(r'\badoptPtr\b', line)
+    if matched_create_dc and not matched_own_dc:
+        error(line_number, 'runtime/leaky_pattern', 5,
+              'Use adoptPtr and OwnPtr<HDC> when calling %s to avoid potential '
+              'memory leaks.' % matched_create_dc.group('function_name'))
+
+
+def check_spacing(file_extension, clean_lines, line_number, error):
+    """Checks for the correctness of various spacing issues in the code.
+
+    Things we check for: spaces around operators, spaces after
+    if/for/while/switch, no spaces around parens in function calls, two
+    spaces between code and comment, don't start a block with a blank
+    line, don't end a function with a blank line, don't have too many
+    blank lines in a row.
+
+    Args:
+      file_extension: The current file extension, without the leading dot.
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      error: The function to call with any errors found.
+    """
+
+    raw = clean_lines.raw_lines
+    line = raw[line_number]
+
+    # Before nixing comments, check if the line is blank for no good
+    # reason.  This includes the first line after a block is opened, and
+    # blank lines at the end of a function (ie, right before a line like '}').
+    if is_blank_line(line):
+        elided = clean_lines.elided
+        previous_line = elided[line_number - 1]
+        previous_brace = previous_line.rfind('{')
+        # FIXME: Don't complain if line before blank line, and line after,
+        #        both start with alnums and are indented the same amount.
+        #        This ignores whitespace at the start of a namespace block
+        #        because those are not usually indented.
+        if (previous_brace != -1 and previous_line[previous_brace:].find('}') == -1
+            and previous_line[:previous_brace].find('namespace') == -1):
+            # OK, we have a blank line at the start of a code block.  Before we
+            # complain, we check if it is an exception to the rule: The previous
+            # non-empty line has the parameters of a function header that are indented
+            # 4 spaces (because they did not fit in a 80 column line when placed on
+            # the same line as the function name).  We also check for the case where
+            # the previous line is indented 6 spaces, which may happen when the
+            # initializers of a constructor do not fit into a 80 column line.
+            exception = False
+            if match(r' {6}\w', previous_line):  # Initializer list?
+                # We are looking for the opening column of initializer list, which
+                # should be indented 4 spaces to cause 6 space indentation afterwards.
+                search_position = line_number - 2
+                while (search_position >= 0
+                       and match(r' {6}\w', elided[search_position])):
+                    search_position -= 1
+                exception = (search_position >= 0
+                             and elided[search_position][:5] == '    :')
+            else:
+                # Search for the function arguments or an initializer list.  We use a
+                # simple heuristic here: If the line is indented 4 spaces; and we have a
+                # closing paren, without the opening paren, followed by an opening brace
+                # or colon (for initializer lists) we assume that it is the last line of
+                # a function header.  If we have a colon indented 4 spaces, it is an
+                # initializer list.
+                exception = (match(r' {4}\w[^\(]*\)\s*(const\s*)?(\{\s*$|:)',
+                                   previous_line)
+                             or match(r' {4}:', previous_line))
+
+            if not exception:
+                error(line_number, 'whitespace/blank_line', 2,
+                      'Blank line at the start of a code block.  Is this needed?')
+        # This doesn't ignore whitespace at the end of a namespace block
+        # because that is too hard without pairing open/close braces;
+        # however, a special exception is made for namespace closing
+        # brackets which have a comment containing "namespace".
+        #
+        # Also, ignore blank lines at the end of a block in a long if-else
+        # chain, like this:
+        #   if (condition1) {
+        #     // Something followed by a blank line
+        #
+        #   } else if (condition2) {
+        #     // Something else
+        #   }
+        if line_number + 1 < clean_lines.num_lines():
+            next_line = raw[line_number + 1]
+            if (next_line
+                and match(r'\s*}', next_line)
+                and next_line.find('namespace') == -1
+                and next_line.find('} else ') == -1):
+                error(line_number, 'whitespace/blank_line', 3,
+                      'Blank line at the end of a code block.  Is this needed?')
+
+    # Next, we check for proper spacing with respect to comments.
+    comment_position = line.find('//')
+    if comment_position != -1:
+        # Check if the // may be in quotes.  If so, ignore it
+        # Comparisons made explicit for clarity -- pylint: disable-msg=C6403
+        if (line.count('"', 0, comment_position) - line.count('\\"', 0, comment_position)) % 2 == 0:   # not in quotes
+            # Allow one space before end of line comment.
+            if (not match(r'^\s*$', line[:comment_position])
+                and (comment_position >= 1
+                and ((line[comment_position - 1] not in string.whitespace)
+                     or (comment_position >= 2
+                         and line[comment_position - 2] in string.whitespace)))):
+                error(line_number, 'whitespace/comments', 5,
+                      'One space before end of line comments')
+            # There should always be a space between the // and the comment
+            commentend = comment_position + 2
+            if commentend < len(line) and not line[commentend] == ' ':
+                # but some lines are exceptions -- e.g. if they're big
+                # comment delimiters like:
+                # //----------------------------------------------------------
+                # or they begin with multiple slashes followed by a space:
+                # //////// Header comment
+                matched = (search(r'[=/-]{4,}\s*$', line[commentend:])
+                           or search(r'^/+ ', line[commentend:]))
+                if not matched:
+                    error(line_number, 'whitespace/comments', 4,
+                          'Should have a space between // and comment')
+
+            # There should only be one space after punctuation in a comment.
+            if search(r'[.!?,;:]\s\s+\w', line[comment_position:]):
+                error(line_number, 'whitespace/comments', 5,
+                      'Should have only a single space after a punctuation in a comment.')
+
+    line = clean_lines.elided[line_number]  # get rid of comments and strings
+
+    # Don't try to do spacing checks for operator methods
+    line = sub(r'operator(==|!=|<|<<|<=|>=|>>|>|\+=|-=|\*=|/=|%=|&=|\|=|^=|<<=|>>=|/)\(', 'operator\(', line)
+    # Don't try to do spacing checks for #include or #import statements at
+    # minimum because it messes up checks for spacing around /
+    if match(r'\s*#\s*(?:include|import)', line):
+        return
+    if search(r'[\w.]=[\w.]', line):
+        error(line_number, 'whitespace/operators', 4,
+              'Missing spaces around =')
+
+    # FIXME: It's not ok to have spaces around binary operators like .
+
+    # You should always have whitespace around binary operators.
+    # Alas, we can't test < or > because they're legitimately used sans spaces
+    # (a->b, vector<int> a).  The only time we can tell is a < with no >, and
+    # only if it's not template params list spilling into the next line.
+    matched = search(r'[^<>=!\s](==|!=|\+=|-=|\*=|/=|/|\|=|&=|<<=|>>=|<=|>=|\|\||\||&&|>>|<<)[^<>=!\s]', line)
+    if not matched:
+        # Note that while it seems that the '<[^<]*' term in the following
+        # regexp could be simplified to '<.*', which would indeed match
+        # the same class of strings, the [^<] means that searching for the
+        # regexp takes linear rather than quadratic time.
+        if not search(r'<[^<]*,\s*$', line):  # template params spill
+            matched = search(r'[^<>=!\s](<)[^<>=!\s]([^>]|->)*$', line)
+    if matched:
+        error(line_number, 'whitespace/operators', 3,
+              'Missing spaces around %s' % matched.group(1))
+
+    # There shouldn't be space around unary operators
+    matched = search(r'(!\s|~\s|[\s]--[\s;]|[\s]\+\+[\s;])', line)
+    if matched:
+        error(line_number, 'whitespace/operators', 4,
+              'Extra space for operator %s' % matched.group(1))
+
+    # A pet peeve of mine: no spaces after an if, while, switch, or for
+    matched = search(r' (if\(|for\(|foreach\(|while\(|switch\()', line)
+    if matched:
+        error(line_number, 'whitespace/parens', 5,
+              'Missing space before ( in %s' % matched.group(1))
+
+    # For if/for/foreach/while/switch, the left and right parens should be
+    # consistent about how many spaces are inside the parens, and
+    # there should either be zero or one spaces inside the parens.
+    # We don't want: "if ( foo)" or "if ( foo   )".
+    # Exception: "for ( ; foo; bar)" and "for (foo; bar; )" are allowed.
+    matched = search(r'\b(?P<statement>if|for|foreach|while|switch)\s*\((?P<remainder>.*)$', line)
+    if matched:
+        statement = matched.group('statement')
+        condition, rest = up_to_unmatched_closing_paren(matched.group('remainder'))
+        if condition is not None:
+            condition_match = search(r'(?P<leading>[ ]*)(?P<separator>.).*[^ ]+(?P<trailing>[ ]*)', condition)
+            if condition_match:
+                n_leading = len(condition_match.group('leading'))
+                n_trailing = len(condition_match.group('trailing'))
+                if n_leading != 0:
+                    for_exception = statement == 'for' and condition.startswith(' ;')
+                    if not for_exception:
+                        error(line_number, 'whitespace/parens', 5,
+                              'Extra space after ( in %s' % statement)
+                if n_trailing != 0:
+                    for_exception = statement == 'for' and condition.endswith('; ')
+                    if not for_exception:
+                        error(line_number, 'whitespace/parens', 5,
+                              'Extra space before ) in %s' % statement)
+
+            # Do not check for more than one command in macros
+            in_preprocessor_directive = match(r'\s*#', line)
+            if not in_preprocessor_directive and not match(r'((\s*{\s*}?)|(\s*;?))\s*\\?$', rest):
+                error(line_number, 'whitespace/parens', 4,
+                      'More than one command on the same line in %s' % statement)
+
+    # You should always have a space after a comma (either as fn arg or operator)
+    if search(r',[^\s]', line):
+        error(line_number, 'whitespace/comma', 3,
+              'Missing space after ,')
+
+    matched = search(r'^\s*(?P<token1>[a-zA-Z0-9_\*&]+)\s\s+(?P<token2>[a-zA-Z0-9_\*&]+)', line)
+    if matched:
+        error(line_number, 'whitespace/declaration', 3,
+              'Extra space between %s and %s' % (matched.group('token1'), matched.group('token2')))
+
+    if file_extension == 'cpp':
+        # C++ should have the & or * beside the type not the variable name.
+        matched = match(r'\s*\w+(?<!\breturn|\bdelete)\s+(?P<pointer_operator>\*|\&)\w+', line)
+        if matched:
+            error(line_number, 'whitespace/declaration', 3,
+                  'Declaration has space between type name and %s in %s' % (matched.group('pointer_operator'), matched.group(0).strip()))
+
+    elif file_extension == 'c':
+        # C Pointer declaration should have the * beside the variable not the type name.
+        matched = search(r'^\s*\w+\*\s+\w+', line)
+        if matched:
+            error(line_number, 'whitespace/declaration', 3,
+                  'Declaration has space between * and variable name in %s' % matched.group(0).strip())
+
+    # Next we will look for issues with function calls.
+    check_spacing_for_function_call(line, line_number, error)
+
+    # Except after an opening paren, you should have spaces before your braces.
+    # And since you should never have braces at the beginning of a line, this is
+    # an easy test.
+    if search(r'[^ ({]{', line):
+        error(line_number, 'whitespace/braces', 5,
+              'Missing space before {')
+
+    # Make sure '} else {' has spaces.
+    if search(r'}else', line):
+        error(line_number, 'whitespace/braces', 5,
+              'Missing space before else')
+
+    # You shouldn't have spaces before your brackets, except maybe after
+    # 'delete []' or 'new char * []'.
+    if search(r'\w\s+\[', line) and not search(r'delete\s+\[', line):
+        error(line_number, 'whitespace/braces', 5,
+              'Extra space before [')
+
+    # There should always be a single space in between braces on the same line.
+    if search(r'\{\}', line):
+        error(line_number, 'whitespace/braces', 5, 'Missing space inside { }.')
+    if search(r'\{\s\s+\}', line):
+        error(line_number, 'whitespace/braces', 5, 'Too many spaces inside { }.')
+
+    # You shouldn't have a space before a semicolon at the end of the line.
+    # There's a special case for "for" since the style guide allows space before
+    # the semicolon there.
+    if search(r':\s*;\s*$', line):
+        error(line_number, 'whitespace/semicolon', 5,
+              'Semicolon defining empty statement. Use { } instead.')
+    elif search(r'^\s*;\s*$', line):
+        error(line_number, 'whitespace/semicolon', 5,
+              'Line contains only semicolon. If this should be an empty statement, '
+              'use { } instead.')
+    elif (search(r'\s+;\s*$', line) and not search(r'\bfor\b', line)):
+        error(line_number, 'whitespace/semicolon', 5,
+              'Extra space before last semicolon. If this should be an empty '
+              'statement, use { } instead.')
+    elif (search(r'\b(for|while)\s*\(.*\)\s*;\s*$', line)
+          and line.count('(') == line.count(')')
+          # Allow do {} while();
+          and not search(r'}\s*while', line)):
+        error(line_number, 'whitespace/semicolon', 5,
+              'Semicolon defining empty statement for this loop. Use { } instead.')
+
+
+def get_previous_non_blank_line(clean_lines, line_number):
+    """Return the most recent non-blank line and its line number.
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file contents.
+      line_number: The number of the line to check.
+
+    Returns:
+      A tuple with two elements.  The first element is the contents of the last
+      non-blank line before the current line, or the empty string if this is the
+      first non-blank line.  The second is the line number of that line, or -1
+      if this is the first non-blank line.
+    """
+
+    previous_line_number = line_number - 1
+    while previous_line_number >= 0:
+        previous_line = clean_lines.elided[previous_line_number]
+        if not is_blank_line(previous_line):     # if not a blank line...
+            return (previous_line, previous_line_number)
+        previous_line_number -= 1
+    return ('', -1)
+
+
+def check_namespace_indentation(clean_lines, line_number, file_extension, file_state, error):
+    """Looks for indentation errors inside of namespaces.
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      file_extension: The extension (dot not included) of the file.
+      file_state: A _FileState instance which maintains information about
+                  the state of things in the file.
+      error: The function to call with any errors found.
+    """
+
+    line = clean_lines.elided[line_number] # Get rid of comments and strings.
+
+    namespace_match = match(r'(?P<namespace_indentation>\s*)namespace\s+\S+\s*{\s*$', line)
+    if not namespace_match:
+        return
+
+    current_indentation_level = len(namespace_match.group('namespace_indentation'))
+    if current_indentation_level > 0:
+        # Don't warn about an indented namespace if we already warned about indented code.
+        if not file_state.did_inside_namespace_indent_warning():
+            error(line_number, 'whitespace/indent', 4,
+                  'namespace should never be indented.')
+        return
+    looking_for_semicolon = False;
+    line_offset = 0
+    in_preprocessor_directive = False;
+    for current_line in clean_lines.elided[line_number + 1:]:
+        line_offset += 1
+        if not current_line.strip():
+            continue
+        if not current_indentation_level:
+            if not (in_preprocessor_directive or looking_for_semicolon):
+                if not match(r'\S', current_line) and not file_state.did_inside_namespace_indent_warning():
+                    file_state.set_did_inside_namespace_indent_warning()
+                    error(line_number + line_offset, 'whitespace/indent', 4,
+                          'Code inside a namespace should not be indented.')
+            if in_preprocessor_directive or (current_line.strip()[0] == '#'): # This takes care of preprocessor directive syntax.
+                in_preprocessor_directive = current_line[-1] == '\\'
+            else:
+                looking_for_semicolon = ((current_line.find(';') == -1) and (current_line.strip()[-1] != '}')) or (current_line[-1] == '\\')
+        else:
+            looking_for_semicolon = False; # If we have a brace we may not need a semicolon.
+        current_indentation_level += current_line.count('{') - current_line.count('}')
+        if current_indentation_level < 0:
+            break;
+
+
+def check_directive_indentation(clean_lines, line_number, file_state, error):
+    """Looks for indentation of preprocessor directives.
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      file_state: A _FileState instance which maintains information about
+                  the state of things in the file.
+      error: The function to call with any errors found.
+    """
+
+    line = clean_lines.elided[line_number]  # Get rid of comments and strings.
+
+    indented_preprocessor_directives = match(r'\s+#', line)
+    if not indented_preprocessor_directives:
+        return
+
+    error(line_number, 'whitespace/indent', 4, 'preprocessor directives (e.g., #ifdef, #define, #import) should never be indented.')
+
+
+def get_initial_spaces_for_line(clean_line):
+    initial_spaces = 0
+    while initial_spaces < len(clean_line) and clean_line[initial_spaces] == ' ':
+        initial_spaces += 1
+    return initial_spaces
+
+
+def check_indentation_amount(clean_lines, line_number, error):
+    line = clean_lines.elided[line_number]
+    initial_spaces = get_initial_spaces_for_line(line)
+
+    if initial_spaces % 4:
+        error(line_number, 'whitespace/indent', 3,
+              'Weird number of spaces at line-start.  Are you using a 4-space indent?')
+        return
+
+    previous_line = get_previous_non_blank_line(clean_lines, line_number)[0]
+    if not previous_line.strip() or match(r'\s*\w+\s*:\s*$', previous_line) or previous_line[0] == '#':
+        return
+
+    previous_line_initial_spaces = get_initial_spaces_for_line(previous_line)
+    if initial_spaces > previous_line_initial_spaces + 4:
+        error(line_number, 'whitespace/indent', 3, 'When wrapping a line, only indent 4 spaces.')
+
+
+def check_using_std(clean_lines, line_number, file_state, error):
+    """Looks for 'using std::foo;' statements which should be replaced with 'using namespace std;'.
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      file_state: A _FileState instance which maintains information about
+                  the state of things in the file.
+      error: The function to call with any errors found.
+    """
+
+    # This check doesn't apply to C or Objective-C implementation files.
+    if file_state.is_c_or_objective_c():
+        return
+
+    line = clean_lines.elided[line_number] # Get rid of comments and strings.
+
+    using_std_match = match(r'\s*using\s+std::(?P<method_name>\S+)\s*;\s*$', line)
+    if not using_std_match:
+        return
+
+    method_name = using_std_match.group('method_name')
+    error(line_number, 'build/using_std', 4,
+          "Use 'using namespace std;' instead of 'using std::%s;'." % method_name)
+
+
+def check_max_min_macros(clean_lines, line_number, file_state, error):
+    """Looks use of MAX() and MIN() macros that should be replaced with std::max() and std::min().
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      file_state: A _FileState instance which maintains information about
+                  the state of things in the file.
+      error: The function to call with any errors found.
+    """
+
+    # This check doesn't apply to C or Objective-C implementation files.
+    if file_state.is_c_or_objective_c():
+        return
+
+    line = clean_lines.elided[line_number] # Get rid of comments and strings.
+
+    max_min_macros_search = search(r'\b(?P<max_min_macro>(MAX|MIN))\s*\(', line)
+    if not max_min_macros_search:
+        return
+
+    max_min_macro = max_min_macros_search.group('max_min_macro')
+    max_min_macro_lower = max_min_macro.lower()
+    error(line_number, 'runtime/max_min_macros', 4,
+          'Use std::%s() or std::%s<type>() instead of the %s() macro.'
+          % (max_min_macro_lower, max_min_macro_lower, max_min_macro))
+
+
+def check_ctype_functions(clean_lines, line_number, file_state, error):
+    """Looks for use of the standard functions in ctype.h and suggest they be replaced
+       by use of equivilent ones in <wtf/ASCIICType.h>?.
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      file_state: A _FileState instance which maintains information about
+                  the state of things in the file.
+      error: The function to call with any errors found.
+    """
+
+    line = clean_lines.elided[line_number]  # Get rid of comments and strings.
+
+    ctype_function_search = search(r'\b(?P<ctype_function>(isalnum|isalpha|isascii|isblank|iscntrl|isdigit|isgraph|islower|isprint|ispunct|isspace|isupper|isxdigit|toascii|tolower|toupper))\s*\(', line)
+    if not ctype_function_search:
+        return
+
+    ctype_function = ctype_function_search.group('ctype_function')
+    error(line_number, 'runtime/ctype_function', 4,
+          'Use equivelent function in <wtf/ASCIICType.h> instead of the %s() function.'
+          % (ctype_function))
+
+def check_switch_indentation(clean_lines, line_number, error):
+    """Looks for indentation errors inside of switch statements.
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      error: The function to call with any errors found.
+    """
+
+    line = clean_lines.elided[line_number] # Get rid of comments and strings.
+
+    switch_match = match(r'(?P<switch_indentation>\s*)switch\s*\(.+\)\s*{\s*$', line)
+    if not switch_match:
+        return
+
+    switch_indentation = switch_match.group('switch_indentation')
+    inner_indentation = switch_indentation + ' ' * 4
+    line_offset = 0
+    encountered_nested_switch = False
+
+    for current_line in clean_lines.elided[line_number + 1:]:
+        line_offset += 1
+
+        # Skip not only empty lines but also those with preprocessor directives.
+        if current_line.strip() == '' or current_line.startswith('#'):
+            continue
+
+        if match(r'\s*switch\s*\(.+\)\s*{\s*$', current_line):
+            # Complexity alarm - another switch statement nested inside the one
+            # that we're currently testing. We'll need to track the extent of
+            # that inner switch if the upcoming label tests are still supposed
+            # to work correctly. Let's not do that; instead, we'll finish
+            # checking this line, and then leave it like that. Assuming the
+            # indentation is done consistently (even if incorrectly), this will
+            # still catch all indentation issues in practice.
+            encountered_nested_switch = True
+
+        current_indentation_match = match(r'(?P<indentation>\s*)(?P<remaining_line>.*)$', current_line);
+        current_indentation = current_indentation_match.group('indentation')
+        remaining_line = current_indentation_match.group('remaining_line')
+
+        # End the check at the end of the switch statement.
+        if remaining_line.startswith('}') and current_indentation == switch_indentation:
+            break
+        # Case and default branches should not be indented. The regexp also
+        # catches single-line cases like "default: break;" but does not trigger
+        # on stuff like "Document::Foo();".
+        elif match(r'(default|case\s+.*)\s*:([^:].*)?$', remaining_line):
+            if current_indentation != switch_indentation:
+                error(line_number + line_offset, 'whitespace/indent', 4,
+                      'A case label should not be indented, but line up with its switch statement.')
+                # Don't throw an error for multiple badly indented labels,
+                # one should be enough to figure out the problem.
+                break
+        # We ignore goto labels at the very beginning of a line.
+        elif match(r'\w+\s*:\s*$', remaining_line):
+            continue
+        # It's not a goto label, so check if it's indented at least as far as
+        # the switch statement plus one more level of indentation.
+        elif not current_indentation.startswith(inner_indentation):
+            error(line_number + line_offset, 'whitespace/indent', 4,
+                  'Non-label code inside switch statements should be indented.')
+            # Don't throw an error for multiple badly indented statements,
+            # one should be enough to figure out the problem.
+            break
+
+        if encountered_nested_switch:
+            break
+
+
+def check_braces(clean_lines, line_number, error):
+    """Looks for misplaced braces (e.g. at the end of line).
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      error: The function to call with any errors found.
+    """
+
+    line = clean_lines.elided[line_number] # Get rid of comments and strings.
+
+    if match(r'\s*{\s*$', line):
+        # We allow an open brace to start a line in the case where someone
+        # is using braces for function definition or in a block to
+        # explicitly create a new scope, which is commonly used to control
+        # the lifetime of stack-allocated variables.  We don't detect this
+        # perfectly: we just don't complain if the last non-whitespace
+        # character on the previous non-blank line is ';', ':', '{', '}',
+        # ')', or ') const' and doesn't begin with 'if|for|while|switch|else'.
+        # We also allow '#' for #endif and '=' for array initialization.
+        previous_line = get_previous_non_blank_line(clean_lines, line_number)[0]
+        if ((not search(r'[;:}{)=]\s*$|\)\s*((const|OVERRIDE)\s*)*\s*$', previous_line)
+             or search(r'\b(if|for|foreach|while|switch|else)\b', previous_line))
+            and previous_line.find('#') < 0):
+            error(line_number, 'whitespace/braces', 4,
+                  'This { should be at the end of the previous line')
+    elif (search(r'\)\s*(((const|OVERRIDE)\s*)*\s*)?{\s*$', line)
+          and line.count('(') == line.count(')')
+          and not search(r'\b(if|for|foreach|while|switch)\b', line)
+          and not match(r'\s+[A-Z_][A-Z_0-9]+\b', line)):
+        error(line_number, 'whitespace/braces', 4,
+              'Place brace on its own line for function definitions.')
+
+    if (match(r'\s*}\s*(else\s*({\s*)?)?$', line) and line_number > 1):
+        # We check if a closed brace has started a line to see if a
+        # one line control statement was previous.
+        previous_line = clean_lines.elided[line_number - 2]
+        last_open_brace = previous_line.rfind('{')
+        if (last_open_brace != -1 and previous_line.find('}', last_open_brace) == -1
+            and search(r'\b(if|for|foreach|while|else)\b', previous_line)):
+            error(line_number, 'whitespace/braces', 4,
+                  'One line control clauses should not use braces.')
+
+    # An else clause should be on the same line as the preceding closing brace.
+    if match(r'\s*else\s*', line):
+        previous_line = get_previous_non_blank_line(clean_lines, line_number)[0]
+        if match(r'\s*}\s*$', previous_line):
+            error(line_number, 'whitespace/newline', 4,
+                  'An else should appear on the same line as the preceding }')
+
+    # Likewise, an else should never have the else clause on the same line
+    if search(r'\belse [^\s{]', line) and not search(r'\belse if\b', line):
+        error(line_number, 'whitespace/newline', 4,
+              'Else clause should never be on same line as else (use 2 lines)')
+
+    # In the same way, a do/while should never be on one line
+    if match(r'\s*do [^\s{]', line):
+        error(line_number, 'whitespace/newline', 4,
+              'do/while clauses should not be on a single line')
+
+    # Braces shouldn't be followed by a ; unless they're defining a struct
+    # or initializing an array.
+    # We can't tell in general, but we can for some common cases.
+    previous_line_number = line_number
+    while True:
+        (previous_line, previous_line_number) = get_previous_non_blank_line(clean_lines, previous_line_number)
+        if match(r'\s+{.*}\s*;', line) and not previous_line.count(';'):
+            line = previous_line + line
+        else:
+            break
+    if (search(r'{.*}\s*;', line)
+        and line.count('{') == line.count('}')
+        and not search(r'struct|class|enum|\s*=\s*{', line)):
+        error(line_number, 'readability/braces', 4,
+              "You don't need a ; after a }")
+
+
+def check_exit_statement_simplifications(clean_lines, line_number, error):
+    """Looks for else or else-if statements that should be written as an
+    if statement when the prior if concludes with a return, break, continue or
+    goto statement.
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      error: The function to call with any errors found.
+    """
+
+    line = clean_lines.elided[line_number] # Get rid of comments and strings.
+
+    else_match = match(r'(?P<else_indentation>\s*)(\}\s*)?else(\s+if\s*\(|(?P<else>\s*(\{\s*)?\Z))', line)
+    if not else_match:
+        return
+
+    else_indentation = else_match.group('else_indentation')
+    inner_indentation = else_indentation + ' ' * 4
+
+    previous_lines = clean_lines.elided[:line_number]
+    previous_lines.reverse()
+    line_offset = 0
+    encountered_exit_statement = False
+
+    for current_line in previous_lines:
+        line_offset -= 1
+
+        # Skip not only empty lines but also those with preprocessor directives
+        # and goto labels.
+        if current_line.strip() == '' or current_line.startswith('#') or match(r'\w+\s*:\s*$', current_line):
+            continue
+
+        # Skip lines with closing braces on the original indentation level.
+        # Even though the styleguide says they should be on the same line as
+        # the "else if" statement, we also want to check for instances where
+        # the current code does not comply with the coding style. Thus, ignore
+        # these lines and proceed to the line before that.
+        if current_line == else_indentation + '}':
+            continue
+
+        current_indentation_match = match(r'(?P<indentation>\s*)(?P<remaining_line>.*)$', current_line);
+        current_indentation = current_indentation_match.group('indentation')
+        remaining_line = current_indentation_match.group('remaining_line')
+
+        # As we're going up the lines, the first real statement to encounter
+        # has to be an exit statement (return, break, continue or goto) -
+        # otherwise, this check doesn't apply.
+        if not encountered_exit_statement:
+            # We only want to find exit statements if they are on exactly
+            # the same level of indentation as expected from the code inside
+            # the block. If the indentation doesn't strictly match then we
+            # might have a nested if or something, which must be ignored.
+            if current_indentation != inner_indentation:
+                break
+            if match(r'(return(\W+.*)|(break|continue)\s*;|goto\s*\w+;)$', remaining_line):
+                encountered_exit_statement = True
+                continue
+            break
+
+        # When code execution reaches this point, we've found an exit statement
+        # as last statement of the previous block. Now we only need to make
+        # sure that the block belongs to an "if", then we can throw an error.
+
+        # Skip lines with opening braces on the original indentation level,
+        # similar to the closing braces check above. ("if (condition)\n{")
+        if current_line == else_indentation + '{':
+            continue
+
+        # Skip everything that's further indented than our "else" or "else if".
+        if current_indentation.startswith(else_indentation) and current_indentation != else_indentation:
+            continue
+
+        # So we've got a line with same (or less) indentation. Is it an "if"?
+        # If yes: throw an error. If no: don't throw an error.
+        # Whatever the outcome, this is the end of our loop.
+        if match(r'if\s*\(', remaining_line):
+            if else_match.start('else') != -1:
+                error(line_number + line_offset, 'readability/control_flow', 4,
+                      'An else statement can be removed when the prior "if" '
+                      'concludes with a return, break, continue or goto statement.')
+            else:
+                error(line_number + line_offset, 'readability/control_flow', 4,
+                      'An else if statement should be written as an if statement '
+                      'when the prior "if" concludes with a return, break, '
+                      'continue or goto statement.')
+        break
+
+
+def replaceable_check(operator, macro, line):
+    """Determine whether a basic CHECK can be replaced with a more specific one.
+
+    For example suggest using CHECK_EQ instead of CHECK(a == b) and
+    similarly for CHECK_GE, CHECK_GT, CHECK_LE, CHECK_LT, CHECK_NE.
+
+    Args:
+      operator: The C++ operator used in the CHECK.
+      macro: The CHECK or EXPECT macro being called.
+      line: The current source line.
+
+    Returns:
+      True if the CHECK can be replaced with a more specific one.
+    """
+
+    # This matches decimal and hex integers, strings, and chars (in that order).
+    match_constant = r'([-+]?(\d+|0[xX][0-9a-fA-F]+)[lLuU]{0,3}|".*"|\'.*\')'
+
+    # Expression to match two sides of the operator with something that
+    # looks like a literal, since CHECK(x == iterator) won't compile.
+    # This means we can't catch all the cases where a more specific
+    # CHECK is possible, but it's less annoying than dealing with
+    # extraneous warnings.
+    match_this = (r'\s*' + macro + r'\((\s*' +
+                  match_constant + r'\s*' + operator + r'[^<>].*|'
+                  r'.*[^<>]' + operator + r'\s*' + match_constant +
+                  r'\s*\))')
+
+    # Don't complain about CHECK(x == NULL) or similar because
+    # CHECK_EQ(x, NULL) won't compile (requires a cast).
+    # Also, don't complain about more complex boolean expressions
+    # involving && or || such as CHECK(a == b || c == d).
+    return match(match_this, line) and not search(r'NULL|&&|\|\|', line)
+
+
+def check_check(clean_lines, line_number, error):
+    """Checks the use of CHECK and EXPECT macros.
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      error: The function to call with any errors found.
+    """
+
+    # Decide the set of replacement macros that should be suggested
+    raw_lines = clean_lines.raw_lines
+    current_macro = ''
+    for macro in _CHECK_MACROS:
+        if raw_lines[line_number].find(macro) >= 0:
+            current_macro = macro
+            break
+    if not current_macro:
+        # Don't waste time here if line doesn't contain 'CHECK' or 'EXPECT'
+        return
+
+    line = clean_lines.elided[line_number]        # get rid of comments and strings
+
+    # Encourage replacing plain CHECKs with CHECK_EQ/CHECK_NE/etc.
+    for operator in ['==', '!=', '>=', '>', '<=', '<']:
+        if replaceable_check(operator, current_macro, line):
+            error(line_number, 'readability/check', 2,
+                  'Consider using %s instead of %s(a %s b)' % (
+                      _CHECK_REPLACEMENT[current_macro][operator],
+                      current_macro, operator))
+            break
+
+
+def check_for_comparisons_to_zero(clean_lines, line_number, error):
+    # Get the line without comments and strings.
+    line = clean_lines.elided[line_number]
+
+    # Include NULL here so that users don't have to convert NULL to 0 first and then get this error.
+    if search(r'[=!]=\s*(NULL|0|true|false)[^\w.]', line) or search(r'[^\w.](NULL|0|true|false)\s*[=!]=', line):
+        if not search('LIKELY', line) and not search('UNLIKELY', line):
+            error(line_number, 'readability/comparison_to_zero', 5,
+                  'Tests for true/false, null/non-null, and zero/non-zero should all be done without equality comparisons.')
+
+
+def check_for_null(clean_lines, line_number, file_state, error):
+    # This check doesn't apply to C or Objective-C implementation files.
+    if file_state.is_c_or_objective_c():
+        return
+
+    line = clean_lines.elided[line_number]
+
+    # Don't warn about NULL usage in g_*(). See Bug 32858 and 39372.
+    if search(r'\bg(_[a-z]+)+\b', line):
+        return
+
+    # Don't warn about NULL usage in gst_*(). See Bug 70498.
+    if search(r'\bgst(_[a-z]+)+\b', line):
+        return
+
+    # Don't warn about NULL usage in gdk_pixbuf_save_to_*{join,concat}(). See Bug 43090.
+    if search(r'\bgdk_pixbuf_save_to\w+\b', line):
+        return
+
+    # Don't warn about NULL usage in gtk_widget_style_get() or gtk_style_context_get_style. See Bug 51758
+    if search(r'\bgtk_widget_style_get\(\w+\b', line) or search(r'\bgtk_style_context_get_style\(\w+\b', line):
+        return
+
+    # Don't warn about NULL usage in soup_server_new(). See Bug 77890.
+    if search(r'\bsoup_server_new\(\w+\b', line):
+        return
+
+    if search(r'\bNULL\b', line):
+        error(line_number, 'readability/null', 5, 'Use 0 instead of NULL.')
+        return
+
+    line = clean_lines.raw_lines[line_number]
+    # See if NULL occurs in any comments in the line. If the search for NULL using the raw line
+    # matches, then do the check with strings collapsed to avoid giving errors for
+    # NULLs occurring in strings.
+    if search(r'\bNULL\b', line) and search(r'\bNULL\b', CleansedLines.collapse_strings(line)):
+        error(line_number, 'readability/null', 4, 'Use 0 or null instead of NULL (even in *comments*).')
+
+def get_line_width(line):
+    """Determines the width of the line in column positions.
+
+    Args:
+      line: A string, which may be a Unicode string.
+
+    Returns:
+      The width of the line in column positions, accounting for Unicode
+      combining characters and wide characters.
+    """
+    if isinstance(line, unicode):
+        width = 0
+        for c in unicodedata.normalize('NFC', line):
+            if unicodedata.east_asian_width(c) in ('W', 'F'):
+                width += 2
+            elif not unicodedata.combining(c):
+                width += 1
+        return width
+    return len(line)
+
+
+def check_style(clean_lines, line_number, file_extension, class_state, file_state, error):
+    """Checks rules from the 'C++ style rules' section of cppguide.html.
+
+    Most of these rules are hard to test (naming, comment style), but we
+    do what we can.  In particular we check for 4-space indents, line lengths,
+    tab usage, spaces inside code, etc.
+
+    Args:
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      file_extension: The extension (without the dot) of the filename.
+      class_state: A _ClassState instance which maintains information about
+                   the current stack of nested class declarations being parsed.
+      file_state: A _FileState instance which maintains information about
+                  the state of things in the file.
+      error: The function to call with any errors found.
+    """
+
+    raw_lines = clean_lines.raw_lines
+    line = raw_lines[line_number]
+
+    if line.find('\t') != -1:
+        error(line_number, 'whitespace/tab', 1,
+              'Tab found; better to use spaces')
+
+    cleansed_line = clean_lines.elided[line_number]
+    if line and line[-1].isspace():
+        error(line_number, 'whitespace/end_of_line', 4,
+              'Line ends in whitespace.  Consider deleting these extra spaces.')
+
+    if (cleansed_line.count(';') > 1
+        # for loops are allowed two ;'s (and may run over two lines).
+        and cleansed_line.find('for') == -1
+        and (get_previous_non_blank_line(clean_lines, line_number)[0].find('for') == -1
+             or get_previous_non_blank_line(clean_lines, line_number)[0].find(';') != -1)
+        # It's ok to have many commands in a switch case that fits in 1 line
+        and not ((cleansed_line.find('case ') != -1
+                  or cleansed_line.find('default:') != -1)
+                 and cleansed_line.find('break;') != -1)
+        # Also it's ok to have many commands in trivial single-line accessors in class definitions.
+        and not (match(r'.*\(.*\).*{.*.}', line)
+                 and class_state.classinfo_stack
+                 and line.count('{') == line.count('}'))
+        and not cleansed_line.startswith('#define ')
+        # It's ok to use use WTF_MAKE_NONCOPYABLE and WTF_MAKE_FAST_ALLOCATED macros in 1 line
+        and not (cleansed_line.find("WTF_MAKE_NONCOPYABLE") != -1
+                 and cleansed_line.find("WTF_MAKE_FAST_ALLOCATED") != -1)):
+        error(line_number, 'whitespace/newline', 4,
+              'More than one command on the same line')
+
+    if cleansed_line.strip().endswith('||') or cleansed_line.strip().endswith('&&'):
+        error(line_number, 'whitespace/operators', 4,
+              'Boolean expressions that span multiple lines should have their '
+              'operators on the left side of the line instead of the right side.')
+
+    # Some more style checks
+    check_namespace_indentation(clean_lines, line_number, file_extension, file_state, error)
+    check_directive_indentation(clean_lines, line_number, file_state, error)
+    check_using_std(clean_lines, line_number, file_state, error)
+    check_max_min_macros(clean_lines, line_number, file_state, error)
+    check_ctype_functions(clean_lines, line_number, file_state, error)
+    check_switch_indentation(clean_lines, line_number, error)
+    check_braces(clean_lines, line_number, error)
+    check_exit_statement_simplifications(clean_lines, line_number, error)
+    check_spacing(file_extension, clean_lines, line_number, error)
+    check_check(clean_lines, line_number, error)
+    check_for_comparisons_to_zero(clean_lines, line_number, error)
+    check_for_null(clean_lines, line_number, file_state, error)
+    check_indentation_amount(clean_lines, line_number, error)
+
+
+_RE_PATTERN_INCLUDE_NEW_STYLE = re.compile(r'#include +"[^/]+\.h"')
+_RE_PATTERN_INCLUDE = re.compile(r'^\s*#\s*include\s*([<"])([^>"]*)[>"].*$')
+# Matches the first component of a filename delimited by -s and _s. That is:
+#  _RE_FIRST_COMPONENT.match('foo').group(0) == 'foo'
+#  _RE_FIRST_COMPONENT.match('foo.cpp').group(0) == 'foo'
+#  _RE_FIRST_COMPONENT.match('foo-bar_baz.cpp').group(0) == 'foo'
+#  _RE_FIRST_COMPONENT.match('foo_bar-baz.cpp').group(0) == 'foo'
+_RE_FIRST_COMPONENT = re.compile(r'^[^-_.]+')
+
+
+def _drop_common_suffixes(filename):
+    """Drops common suffixes like _test.cpp or -inl.h from filename.
+
+    For example:
+      >>> _drop_common_suffixes('foo/foo-inl.h')
+      'foo/foo'
+      >>> _drop_common_suffixes('foo/bar/foo.cpp')
+      'foo/bar/foo'
+      >>> _drop_common_suffixes('foo/foo_internal.h')
+      'foo/foo'
+      >>> _drop_common_suffixes('foo/foo_unusualinternal.h')
+      'foo/foo_unusualinternal'
+
+    Args:
+      filename: The input filename.
+
+    Returns:
+      The filename with the common suffix removed.
+    """
+    for suffix in ('test.cpp', 'regtest.cpp', 'unittest.cpp',
+                   'inl.h', 'impl.h', 'internal.h'):
+        if (filename.endswith(suffix) and len(filename) > len(suffix)
+            and filename[-len(suffix) - 1] in ('-', '_')):
+            return filename[:-len(suffix) - 1]
+    return os.path.splitext(filename)[0]
+
+
+def _classify_include(filename, include, is_system, include_state):
+    """Figures out what kind of header 'include' is.
+
+    Args:
+      filename: The current file cpp_style is running over.
+      include: The path to a #included file.
+      is_system: True if the #include used <> rather than "".
+      include_state: An _IncludeState instance in which the headers are inserted.
+
+    Returns:
+      One of the _XXX_HEADER constants.
+
+    For example:
+      >>> _classify_include('foo.cpp', 'config.h', False)
+      _CONFIG_HEADER
+      >>> _classify_include('foo.cpp', 'foo.h', False)
+      _PRIMARY_HEADER
+      >>> _classify_include('foo.cpp', 'bar.h', False)
+      _OTHER_HEADER
+    """
+
+    # If it is a system header we know it is classified as _OTHER_HEADER.
+    if is_system and not include.startswith('public/'):
+        return _OTHER_HEADER
+
+    # If the include is named config.h then this is WebCore/config.h.
+    if include == "config.h":
+        return _CONFIG_HEADER
+
+    # There cannot be primary includes in header files themselves. Only an
+    # include exactly matches the header filename will be is flagged as
+    # primary, so that it triggers the "don't include yourself" check.
+    if filename.endswith('.h') and filename != include:
+        return _OTHER_HEADER;
+
+    # Qt's moc files do not follow the naming and ordering rules, so they should be skipped
+    if include.startswith('moc_') and include.endswith('.cpp'):
+        return _MOC_HEADER
+
+    if include.endswith('.moc'):
+        return _MOC_HEADER
+
+    # If the target file basename starts with the include we're checking
+    # then we consider it the primary header.
+    target_base = FileInfo(filename).base_name()
+    include_base = FileInfo(include).base_name()
+
+    # If we haven't encountered a primary header, then be lenient in checking.
+    if not include_state.visited_primary_section():
+        if target_base.find(include_base) != -1:
+            return _PRIMARY_HEADER
+        # Qt private APIs use _p.h suffix.
+        if include_base.find(target_base) != -1 and include_base.endswith('_p'):
+            return _PRIMARY_HEADER
+
+    # If we already encountered a primary header, perform a strict comparison.
+    # In case the two filename bases are the same then the above lenient check
+    # probably was a false positive.
+    elif include_state.visited_primary_section() and target_base == include_base:
+        if include == "ResourceHandleWin.h":
+            # FIXME: Thus far, we've only seen one example of these, but if we
+            # start to see more, please consider generalizing this check
+            # somehow.
+            return _OTHER_HEADER
+        return _PRIMARY_HEADER
+
+    return _OTHER_HEADER
+
+
+def _does_primary_header_exist(filename):
+    """Return a primary header file name for a file, or empty string
+    if the file is not source file or primary header does not exist.
+    """
+    fileinfo = FileInfo(filename)
+    if not fileinfo.is_source():
+        return False
+    primary_header = fileinfo.no_extension() + ".h"
+    return os.path.isfile(primary_header)
+
+
+def check_include_line(filename, file_extension, clean_lines, line_number, include_state, error):
+    """Check rules that are applicable to #include lines.
+
+    Strings on #include lines are NOT removed from elided line, to make
+    certain tasks easier. However, to prevent false positives, checks
+    applicable to #include lines in CheckLanguage must be put here.
+
+    Args:
+      filename: The name of the current file.
+      file_extension: The current file extension, without the leading dot.
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      include_state: An _IncludeState instance in which the headers are inserted.
+      error: The function to call with any errors found.
+    """
+    # FIXME: For readability or as a possible optimization, consider
+    #        exiting early here by checking whether the "build/include"
+    #        category should be checked for the given filename.  This
+    #        may involve having the error handler classes expose a
+    #        should_check() method, in addition to the usual __call__
+    #        method.
+    line = clean_lines.lines[line_number]
+
+    matched = _RE_PATTERN_INCLUDE.search(line)
+    if not matched:
+        return
+
+    include = matched.group(2)
+    is_system = (matched.group(1) == '<')
+
+    # Look for any of the stream classes that are part of standard C++.
+    if match(r'(f|ind|io|i|o|parse|pf|stdio|str|)?stream$', include):
+        error(line_number, 'readability/streams', 3,
+              'Streams are highly discouraged.')
+
+    # Look for specific includes to fix.
+    if include.startswith('wtf/') and not is_system:
+        error(line_number, 'build/include', 4,
+              'wtf includes should be <wtf/file.h> instead of "wtf/file.h".')
+
+    if filename.find('/chromium/') != -1 and include.startswith('cc/CC'):
+        error(line_number, 'build/include', 4,
+              'cc includes should be "CCFoo.h" instead of "cc/CCFoo.h".')
+
+    duplicate_header = include in include_state
+    if duplicate_header:
+        error(line_number, 'build/include', 4,
+              '"%s" already included at %s:%s' %
+              (include, filename, include_state[include]))
+    else:
+        include_state[include] = line_number
+
+    header_type = _classify_include(filename, include, is_system, include_state)
+    primary_header_exists = _does_primary_header_exist(filename)
+    include_state.header_types[line_number] = header_type
+
+    # Only proceed if this isn't a duplicate header.
+    if duplicate_header:
+        return
+
+    # We want to ensure that headers appear in the right order:
+    # 1) for implementation files: config.h, primary header, blank line, alphabetically sorted
+    # 2) for header files: alphabetically sorted
+    # The include_state object keeps track of the last type seen
+    # and complains if the header types are out of order or missing.
+    error_message = include_state.check_next_include_order(header_type,
+                                                           file_extension == "h",
+                                                           primary_header_exists)
+
+    # Check to make sure we have a blank line after primary header.
+    if not error_message and header_type == _PRIMARY_HEADER:
+         next_line = clean_lines.raw_lines[line_number + 1]
+         if not is_blank_line(next_line):
+            error(line_number, 'build/include_order', 4,
+                  'You should add a blank line after implementation file\'s own header.')
+
+    # Check to make sure all headers besides config.h and the primary header are
+    # alphabetically sorted. Skip Qt's moc files.
+    if not error_message and header_type == _OTHER_HEADER:
+         previous_line_number = line_number - 1;
+         previous_line = clean_lines.lines[previous_line_number]
+         previous_match = _RE_PATTERN_INCLUDE.search(previous_line)
+         while (not previous_match and previous_line_number > 0
+                and not search(r'\A(#if|#ifdef|#ifndef|#else|#elif|#endif)', previous_line)):
+            previous_line_number -= 1;
+            previous_line = clean_lines.lines[previous_line_number]
+            previous_match = _RE_PATTERN_INCLUDE.search(previous_line)
+         if previous_match:
+            previous_header_type = include_state.header_types[previous_line_number]
+            if previous_header_type == _OTHER_HEADER and previous_line.strip() > line.strip():
+                # This type of error is potentially a problem with this line or the previous one,
+                # so if the error is filtered for one line, report it for the next. This is so that
+                # we properly handle patches, for which only modified lines produce errors.
+                if not error(line_number - 1, 'build/include_order', 4, 'Alphabetical sorting problem.'):
+                    error(line_number, 'build/include_order', 4, 'Alphabetical sorting problem.')
+
+    if error_message:
+        if file_extension == 'h':
+            error(line_number, 'build/include_order', 4,
+                  '%s Should be: alphabetically sorted.' %
+                  error_message)
+        else:
+            error(line_number, 'build/include_order', 4,
+                  '%s Should be: config.h, primary header, blank line, and then alphabetically sorted.' %
+                  error_message)
+
+
+def check_language(filename, clean_lines, line_number, file_extension, include_state,
+                   file_state, error):
+    """Checks rules from the 'C++ language rules' section of cppguide.html.
+
+    Some of these rules are hard to test (function overloading, using
+    uint32 inappropriately), but we do the best we can.
+
+    Args:
+      filename: The name of the current file.
+      clean_lines: A CleansedLines instance containing the file.
+      line_number: The number of the line to check.
+      file_extension: The extension (without the dot) of the filename.
+      include_state: An _IncludeState instance in which the headers are inserted.
+      file_state: A _FileState instance which maintains information about
+                  the state of things in the file.
+      error: The function to call with any errors found.
+    """
+    # If the line is empty or consists of entirely a comment, no need to
+    # check it.
+    line = clean_lines.elided[line_number]
+    if not line:
+        return
+
+    matched = _RE_PATTERN_INCLUDE.search(line)
+    if matched:
+        check_include_line(filename, file_extension, clean_lines, line_number, include_state, error)
+        return
+
+    # FIXME: figure out if they're using default arguments in fn proto.
+
+    # Check to see if they're using an conversion function cast.
+    # I just try to capture the most common basic types, though there are more.
+    # Parameterless conversion functions, such as bool(), are allowed as they are
+    # probably a member operator declaration or default constructor.
+    matched = search(
+        r'\b(int|float|double|bool|char|int32|uint32|int64|uint64)\([^)]', line)
+    if matched:
+        # gMock methods are defined using some variant of MOCK_METHODx(name, type)
+        # where type may be float(), int(string), etc.  Without context they are
+        # virtually indistinguishable from int(x) casts.
+        if not match(r'^\s*MOCK_(CONST_)?METHOD\d+(_T)?\(', line):
+            error(line_number, 'readability/casting', 4,
+                  'Using deprecated casting style.  '
+                  'Use static_cast<%s>(...) instead' %
+                  matched.group(1))
+
+    check_c_style_cast(line_number, line, clean_lines.raw_lines[line_number],
+                       'static_cast',
+                       r'\((int|float|double|bool|char|u?int(16|32|64))\)',
+                       error)
+    # This doesn't catch all cases.  Consider (const char * const)"hello".
+    check_c_style_cast(line_number, line, clean_lines.raw_lines[line_number],
+                       'reinterpret_cast', r'\((\w+\s?\*+\s?)\)', error)
+
+    # In addition, we look for people taking the address of a cast.  This
+    # is dangerous -- casts can assign to temporaries, so the pointer doesn't
+    # point where you think.
+    if search(
+        r'(&\([^)]+\)[\w(])|(&(static|dynamic|reinterpret)_cast\b)', line):
+        error(line_number, 'runtime/casting', 4,
+              ('Are you taking an address of a cast?  '
+               'This is dangerous: could be a temp var.  '
+               'Take the address before doing the cast, rather than after'))
+
+    # Check for people declaring static/global STL strings at the top level.
+    # This is dangerous because the C++ language does not guarantee that
+    # globals with constructors are initialized before the first access.
+    matched = match(
+        r'((?:|static +)(?:|const +))string +([a-zA-Z0-9_:]+)\b(.*)',
+        line)
+    # Make sure it's not a function.
+    # Function template specialization looks like: "string foo<Type>(...".
+    # Class template definitions look like: "string Foo<Type>::Method(...".
+    if matched and not match(r'\s*(<.*>)?(::[a-zA-Z0-9_]+)?\s*\(([^"]|$)',
+                             matched.group(3)):
+        error(line_number, 'runtime/string', 4,
+              'For a static/global string constant, use a C style string instead: '
+              '"%schar %s[]".' %
+              (matched.group(1), matched.group(2)))
+
+    # Check that we're not using RTTI outside of testing code.
+    if search(r'\bdynamic_cast<', line):
+        error(line_number, 'runtime/rtti', 5,
+              'Do not use dynamic_cast<>.  If you need to cast within a class '
+              "hierarchy, use static_cast<> to upcast.  Google doesn't support "
+              'RTTI.')
+
+    if search(r'\b([A-Za-z0-9_]*_)\(\1\)', line):
+        error(line_number, 'runtime/init', 4,
+              'You seem to be initializing a member variable with itself.')
+
+    if file_extension == 'h':
+        # FIXME: check that 1-arg constructors are explicit.
+        #        How to tell it's a constructor?
+        #        (handled in check_for_non_standard_constructs for now)
+        pass
+
+    # Check if people are using the verboten C basic types.  The only exception
+    # we regularly allow is "unsigned short port" for port.
+    if search(r'\bshort port\b', line):
+        if not search(r'\bunsigned short port\b', line):
+            error(line_number, 'runtime/int', 4,
+                  'Use "unsigned short" for ports, not "short"')
+
+    # When snprintf is used, the second argument shouldn't be a literal.
+    matched = search(r'snprintf\s*\(([^,]*),\s*([0-9]*)\s*,', line)
+    if matched:
+        error(line_number, 'runtime/printf', 3,
+              'If you can, use sizeof(%s) instead of %s as the 2nd arg '
+              'to snprintf.' % (matched.group(1), matched.group(2)))
+
+    # Check if some verboten C functions are being used.
+    if search(r'\bsprintf\b', line):
+        error(line_number, 'runtime/printf', 5,
+              'Never use sprintf.  Use snprintf instead.')
+    matched = search(r'\b(strcpy|strcat)\b', line)
+    if matched:
+        error(line_number, 'runtime/printf', 4,
+              'Almost always, snprintf is better than %s' % matched.group(1))
+
+    if search(r'\bsscanf\b', line):
+        error(line_number, 'runtime/printf', 1,
+              'sscanf can be ok, but is slow and can overflow buffers.')
+
+    # Check for suspicious usage of "if" like
+    # } if (a == b) {
+    if search(r'\}\s*if\s*\(', line):
+        error(line_number, 'readability/braces', 4,
+              'Did you mean "else if"? If not, start a new line for "if".')
+
+    # Check for potential format string bugs like printf(foo).
+    # We constrain the pattern not to pick things like DocidForPrintf(foo).
+    # Not perfect but it can catch printf(foo.c_str()) and printf(foo->c_str())
+    matched = re.search(r'\b((?:string)?printf)\s*\(([\w.\->()]+)\)', line, re.I)
+    if matched:
+        error(line_number, 'runtime/printf', 4,
+              'Potential format string bug. Do %s("%%s", %s) instead.'
+              % (matched.group(1), matched.group(2)))
+
+    # Check for potential memset bugs like memset(buf, sizeof(buf), 0).
+    matched = search(r'memset\s*\(([^,]*),\s*([^,]*),\s*0\s*\)', line)
+    if matched and not match(r"^''|-?[0-9]+|0x[0-9A-Fa-f]$", matched.group(2)):
+        error(line_number, 'runtime/memset', 4,
+              'Did you mean "memset(%s, 0, %s)"?'
+              % (matched.group(1), matched.group(2)))
+
+    # Detect variable-length arrays.
+    matched = match(r'\s*(.+::)?(\w+) [a-z]\w*\[(.+)];', line)
+    if (matched and matched.group(2) != 'return' and matched.group(2) != 'delete' and
+        matched.group(3).find(']') == -1):
+        # Split the size using space and arithmetic operators as delimiters.
+        # If any of the resulting tokens are not compile time constants then
+        # report the error.
+        tokens = re.split(r'\s|\+|\-|\*|\/|<<|>>]', matched.group(3))
+        is_const = True
+        skip_next = False
+        for tok in tokens:
+            if skip_next:
+                skip_next = False
+                continue
+
+            if search(r'sizeof\(.+\)', tok):
+                continue
+            if search(r'arraysize\(\w+\)', tok):
+                continue
+
+            tok = tok.lstrip('(')
+            tok = tok.rstrip(')')
+            if not tok:
+                continue
+            if match(r'\d+', tok):
+                continue
+            if match(r'0[xX][0-9a-fA-F]+', tok):
+                continue
+            if match(r'k[A-Z0-9]\w*', tok):
+                continue
+            if match(r'(.+::)?k[A-Z0-9]\w*', tok):
+                continue
+            if match(r'(.+::)?[A-Z][A-Z0-9_]*', tok):
+                continue
+            # A catch all for tricky sizeof cases, including 'sizeof expression',
+            # 'sizeof(*type)', 'sizeof(const type)', 'sizeof(struct StructName)'
+            # requires skipping the next token becasue we split on ' ' and '*'.
+            if tok.startswith('sizeof'):
+                skip_next = True
+                continue
+            is_const = False
+            break
+        if not is_const:
+            error(line_number, 'runtime/arrays', 1,
+                  'Do not use variable-length arrays.  Use an appropriately named '
+                  "('k' followed by CamelCase) compile-time constant for the size.")
+
+    # Check for use of unnamed namespaces in header files.  Registration
+    # macros are typically OK, so we allow use of "namespace {" on lines
+    # that end with backslashes.
+    if (file_extension == 'h'
+        and search(r'\bnamespace\s*{', line)
+        and line[-1] != '\\'):
+        error(line_number, 'build/namespaces', 4,
+              'Do not use unnamed namespaces in header files.  See '
+              'http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Namespaces'
+              ' for more information.')
+
+    # Check for plain bitfields declared without either "singed" or "unsigned".
+    # Most compilers treat such bitfields as signed, but there are still compilers like
+    # RVCT 4.0 that use unsigned by default.
+    matched = re.match(r'\s*((const|mutable)\s+)?(char|(short(\s+int)?)|int|long(\s+(long|int))?)\s+[a-zA-Z_][a-zA-Z0-9_]*\s*:\s*\d+\s*;', line)
+    if matched:
+        error(line_number, 'runtime/bitfields', 5,
+              'Please declare integral type bitfields with either signed or unsigned.')
+
+    check_identifier_name_in_declaration(filename, line_number, line, file_state, error)
+
+    # Check for unsigned int (should be just 'unsigned')
+    if search(r'\bunsigned int\b', line):
+        error(line_number, 'runtime/unsigned', 1,
+              'Omit int when using unsigned')
+
+    # Check that we're not using static_cast<Text*>.
+    if search(r'\bstatic_cast<Text\*>', line):
+        error(line_number, 'readability/check', 4,
+              'Consider using toText helper function in WebCore/dom/Text.h '
+              'instead of static_cast<Text*>')
+
+def check_identifier_name_in_declaration(filename, line_number, line, file_state, error):
+    """Checks if identifier names contain any underscores.
+
+    As identifiers in libraries we are using have a bunch of
+    underscores, we only warn about the declarations of identifiers
+    and don't check use of identifiers.
+
+    Args:
+      filename: The name of the current file.
+      line_number: The number of the line to check.
+      line: The line of code to check.
+      file_state: A _FileState instance which maintains information about
+                  the state of things in the file.
+      error: The function to call with any errors found.
+    """
+    # We don't check a return statement.
+    if match(r'\s*(return|delete)\b', line):
+        return
+
+    # Basically, a declaration is a type name followed by whitespaces
+    # followed by an identifier. The type name can be complicated
+    # due to type adjectives and templates. We remove them first to
+    # simplify the process to find declarations of identifiers.
+
+    # Convert "long long", "long double", and "long long int" to
+    # simple types, but don't remove simple "long".
+    line = sub(r'long (long )?(?=long|double|int)', '', line)
+    # Convert unsigned/signed types to simple types, too.
+    line = sub(r'(unsigned|signed) (?=char|short|int|long)', '', line)
+    line = sub(r'\b(inline|using|static|const|volatile|auto|register|extern|typedef|restrict|struct|class|virtual)(?=\W)', '', line)
+
+    # Remove "new" and "new (expr)" to simplify, too.
+    line = sub(r'new\s*(\([^)]*\))?', '', line)
+
+    # Remove all template parameters by removing matching < and >.
+    # Loop until no templates are removed to remove nested templates.
+    while True:
+        line, number_of_replacements = subn(r'<([\w\s:]|::)+\s*[*&]*\s*>', '', line)
+        if not number_of_replacements:
+            break
+
+    # Declarations of local variables can be in condition expressions
+    # of control flow statements (e.g., "if (RenderObject* p = o->parent())").
+    # We remove the keywords and the first parenthesis.
+    #
+    # Declarations in "while", "if", and "switch" are different from
+    # other declarations in two aspects:
+    #
+    # - There can be only one declaration between the parentheses.
+    #   (i.e., you cannot write "if (int i = 0, j = 1) {}")
+    # - The variable must be initialized.
+    #   (i.e., you cannot write "if (int i) {}")
+    #
+    # and we will need different treatments for them.
+    line = sub(r'^\s*for\s*\(', '', line)
+    line, control_statement = subn(r'^\s*(while|else if|if|switch)\s*\(', '', line)
+
+    # Detect variable and functions.
+    type_regexp = r'\w([\w]|\s*[*&]\s*|::)+'
+    identifier_regexp = r'(?P<identifier>[\w:]+)'
+    maybe_bitfield_regexp = r'(:\s*\d+\s*)?'
+    character_after_identifier_regexp = r'(?P<character_after_identifier>[[;()=,])(?!=)'
+    declaration_without_type_regexp = r'\s*' + identifier_regexp + r'\s*' + maybe_bitfield_regexp + character_after_identifier_regexp
+    declaration_with_type_regexp = r'\s*' + type_regexp + r'\s' + declaration_without_type_regexp
+    is_function_arguments = False
+    number_of_identifiers = 0
+    while True:
+        # If we are seeing the first identifier or arguments of a
+        # function, there should be a type name before an identifier.
+        if not number_of_identifiers or is_function_arguments:
+            declaration_regexp = declaration_with_type_regexp
+        else:
+            declaration_regexp = declaration_without_type_regexp
+
+        matched = match(declaration_regexp, line)
+        if not matched:
+            return
+        identifier = matched.group('identifier')
+        character_after_identifier = matched.group('character_after_identifier')
+
+        # If we removed a non-for-control statement, the character after
+        # the identifier should be '='. With this rule, we can avoid
+        # warning for cases like "if (val & INT_MAX) {".
+        if control_statement and character_after_identifier != '=':
+            return
+
+        is_function_arguments = is_function_arguments or character_after_identifier == '('
+
+        # Remove "m_" and "s_" to allow them.
+        modified_identifier = sub(r'(^|(?<=::))[ms]_', '', identifier)
+        if not file_state.is_objective_c() and modified_identifier.find('_') >= 0:
+            # Various exceptions to the rule: JavaScript op codes functions, const_iterator.
+            if (not (filename.find('JavaScriptCore') >= 0 and modified_identifier.find('op_') >= 0)
+                and not (filename.find('gtk') >= 0 and modified_identifier.startswith('webkit_') >= 0)
+                and not modified_identifier.startswith('tst_')
+                and not modified_identifier.startswith('webkit_dom_object_')
+                and not modified_identifier.startswith('webkit_soup')
+                and not modified_identifier.startswith('NPN_')
+                and not modified_identifier.startswith('NPP_')
+                and not modified_identifier.startswith('NP_')
+                and not modified_identifier.startswith('qt_')
+                and not modified_identifier.startswith('_q_')
+                and not modified_identifier.startswith('cairo_')
+                and not modified_identifier.startswith('Ecore_')
+                and not modified_identifier.startswith('Eina_')
+                and not modified_identifier.startswith('Evas_')
+                and not modified_identifier.startswith('Ewk_')
+                and not modified_identifier.startswith('cti_')
+                and not modified_identifier.find('::qt_') >= 0
+                and not modified_identifier.find('::_q_') >= 0
+                and not modified_identifier == "const_iterator"
+                and not modified_identifier == "vm_throw"
+                and not modified_identifier == "DFG_OPERATION"):
+                error(line_number, 'readability/naming/underscores', 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
+        if modified_identifier == 'l':
+            error(line_number, 'readability/naming', 4, identifier + " is incorrectly named. Don't use the single letter 'l' as an identifier name.")
+
+        # There can be only one declaration in non-for-control statements.
+        if control_statement:
+            return
+        # We should continue checking if this is a function
+        # declaration because we need to check its arguments.
+        # Also, we need to check multiple declarations.
+        if character_after_identifier != '(' and character_after_identifier != ',':
+            return
+
+        number_of_identifiers += 1
+        line = line[matched.end():]
+
+def check_c_style_cast(line_number, line, raw_line, cast_type, pattern,
+                       error):
+    """Checks for a C-style cast by looking for the pattern.
+
+    This also handles sizeof(type) warnings, due to similarity of content.
+
+    Args:
+      line_number: The number of the line to check.
+      line: The line of code to check.
+      raw_line: The raw line of code to check, with comments.
+      cast_type: The string for the C++ cast to recommend.  This is either
+                 reinterpret_cast or static_cast, depending.
+      pattern: The regular expression used to find C-style casts.
+      error: The function to call with any errors found.
+    """
+    matched = search(pattern, line)
+    if not matched:
+        return
+
+    # e.g., sizeof(int)
+    sizeof_match = match(r'.*sizeof\s*$', line[0:matched.start(1) - 1])
+    if sizeof_match:
+        error(line_number, 'runtime/sizeof', 1,
+              'Using sizeof(type).  Use sizeof(varname) instead if possible')
+        return
+
+    remainder = line[matched.end(0):]
+
+    # The close paren is for function pointers as arguments to a function.
+    # eg, void foo(void (*bar)(int));
+    # The semicolon check is a more basic function check; also possibly a
+    # function pointer typedef.
+    # eg, void foo(int); or void foo(int) const;
+    # The equals check is for function pointer assignment.
+    # eg, void *(*foo)(int) = ...
+    #
+    # Right now, this will only catch cases where there's a single argument, and
+    # it's unnamed.  It should probably be expanded to check for multiple
+    # arguments with some unnamed.
+    function_match = match(r'\s*(\)|=|(const)?\s*(;|\{|throw\(\)))', remainder)
+    if function_match:
+        if (not function_match.group(3)
+            or function_match.group(3) == ';'
+            or raw_line.find('/*') < 0):
+            error(line_number, 'readability/function', 3,
+                  'All parameters should be named in a function')
+        return
+
+    # At this point, all that should be left is actual casts.
+    error(line_number, 'readability/casting', 4,
+          'Using C-style cast.  Use %s<%s>(...) instead' %
+          (cast_type, matched.group(1)))
+
+
+_HEADERS_CONTAINING_TEMPLATES = (
+    ('<deque>', ('deque',)),
+    ('<functional>', ('unary_function', 'binary_function',
+                      'plus', 'minus', 'multiplies', 'divides', 'modulus',
+                      'negate',
+                      'equal_to', 'not_equal_to', 'greater', 'less',
+                      'greater_equal', 'less_equal',
+                      'logical_and', 'logical_or', 'logical_not',
+                      'unary_negate', 'not1', 'binary_negate', 'not2',
+                      'bind1st', 'bind2nd',
+                      'pointer_to_unary_function',
+                      'pointer_to_binary_function',
+                      'ptr_fun',
+                      'mem_fun_t', 'mem_fun', 'mem_fun1_t', 'mem_fun1_ref_t',
+                      'mem_fun_ref_t',
+                      'const_mem_fun_t', 'const_mem_fun1_t',
+                      'const_mem_fun_ref_t', 'const_mem_fun1_ref_t',
+                      'mem_fun_ref',
+                     )),
+    ('<limits>', ('numeric_limits',)),
+    ('<list>', ('list',)),
+    ('<map>', ('map', 'multimap',)),
+    ('<memory>', ('allocator',)),
+    ('<queue>', ('queue', 'priority_queue',)),
+    ('<set>', ('set', 'multiset',)),
+    ('<stack>', ('stack',)),
+    ('<string>', ('char_traits', 'basic_string',)),
+    ('<utility>', ('pair',)),
+    ('<vector>', ('vector',)),
+
+    # gcc extensions.
+    # Note: std::hash is their hash, ::hash is our hash
+    ('<hash_map>', ('hash_map', 'hash_multimap',)),
+    ('<hash_set>', ('hash_set', 'hash_multiset',)),
+    ('<slist>', ('slist',)),
+    )
+
+_HEADERS_ACCEPTED_BUT_NOT_PROMOTED = {
+    # We can trust with reasonable confidence that map gives us pair<>, too.
+    'pair<>': ('map', 'multimap', 'hash_map', 'hash_multimap')
+}
+
+_RE_PATTERN_STRING = re.compile(r'\bstring\b')
+
+_re_pattern_algorithm_header = []
+for _template in ('copy', 'max', 'min', 'min_element', 'sort', 'swap',
+                  'transform'):
+    # Match max<type>(..., ...), max(..., ...), but not foo->max, foo.max or
+    # type::max().
+    _re_pattern_algorithm_header.append(
+        (re.compile(r'[^>.]\b' + _template + r'(<.*?>)?\([^\)]'),
+         _template,
+         '<algorithm>'))
+
+_re_pattern_templates = []
+for _header, _templates in _HEADERS_CONTAINING_TEMPLATES:
+    for _template in _templates:
+        _re_pattern_templates.append(
+            (re.compile(r'(\<|\b)' + _template + r'\s*\<'),
+             _template + '<>',
+             _header))
+
+
+def files_belong_to_same_module(filename_cpp, filename_h):
+    """Check if these two filenames belong to the same module.
+
+    The concept of a 'module' here is a as follows:
+    foo.h, foo-inl.h, foo.cpp, foo_test.cpp and foo_unittest.cpp belong to the
+    same 'module' if they are in the same directory.
+    some/path/public/xyzzy and some/path/internal/xyzzy are also considered
+    to belong to the same module here.
+
+    If the filename_cpp contains a longer path than the filename_h, for example,
+    '/absolute/path/to/base/sysinfo.cpp', and this file would include
+    'base/sysinfo.h', this function also produces the prefix needed to open the
+    header. This is used by the caller of this function to more robustly open the
+    header file. We don't have access to the real include paths in this context,
+    so we need this guesswork here.
+
+    Known bugs: tools/base/bar.cpp and base/bar.h belong to the same module
+    according to this implementation. Because of this, this function gives
+    some false positives. This should be sufficiently rare in practice.
+
+    Args:
+      filename_cpp: is the path for the .cpp file
+      filename_h: is the path for the header path
+
+    Returns:
+      Tuple with a bool and a string:
+      bool: True if filename_cpp and filename_h belong to the same module.
+      string: the additional prefix needed to open the header file.
+    """
+
+    if not filename_cpp.endswith('.cpp'):
+        return (False, '')
+    filename_cpp = filename_cpp[:-len('.cpp')]
+    if filename_cpp.endswith('_unittest'):
+        filename_cpp = filename_cpp[:-len('_unittest')]
+    elif filename_cpp.endswith('_test'):
+        filename_cpp = filename_cpp[:-len('_test')]
+    filename_cpp = filename_cpp.replace('/public/', '/')
+    filename_cpp = filename_cpp.replace('/internal/', '/')
+
+    if not filename_h.endswith('.h'):
+        return (False, '')
+    filename_h = filename_h[:-len('.h')]
+    if filename_h.endswith('-inl'):
+        filename_h = filename_h[:-len('-inl')]
+    filename_h = filename_h.replace('/public/', '/')
+    filename_h = filename_h.replace('/internal/', '/')
+
+    files_belong_to_same_module = filename_cpp.endswith(filename_h)
+    common_path = ''
+    if files_belong_to_same_module:
+        common_path = filename_cpp[:-len(filename_h)]
+    return files_belong_to_same_module, common_path
+
+
+def update_include_state(filename, include_state, io=codecs):
+    """Fill up the include_state with new includes found from the file.
+
+    Args:
+      filename: the name of the header to read.
+      include_state: an _IncludeState instance in which the headers are inserted.
+      io: The io factory to use to read the file. Provided for testability.
+
+    Returns:
+      True if a header was succesfully added. False otherwise.
+    """
+    io = _unit_test_config.get(INCLUDE_IO_INJECTION_KEY, codecs)
+    header_file = None
+    try:
+        header_file = io.open(filename, 'r', 'utf8', 'replace')
+    except IOError:
+        return False
+    line_number = 0
+    for line in header_file:
+        line_number += 1
+        clean_line = cleanse_comments(line)
+        matched = _RE_PATTERN_INCLUDE.search(clean_line)
+        if matched:
+            include = matched.group(2)
+            # The value formatting is cute, but not really used right now.
+            # What matters here is that the key is in include_state.
+            include_state.setdefault(include, '%s:%d' % (filename, line_number))
+    return True
+
+
+def check_for_include_what_you_use(filename, clean_lines, include_state, error):
+    """Reports for missing stl includes.
+
+    This function will output warnings to make sure you are including the headers
+    necessary for the stl containers and functions that you use. We only give one
+    reason to include a header. For example, if you use both equal_to<> and
+    less<> in a .h file, only one (the latter in the file) of these will be
+    reported as a reason to include the <functional>.
+
+    Args:
+      filename: The name of the current file.
+      clean_lines: A CleansedLines instance containing the file.
+      include_state: An _IncludeState instance.
+      error: The function to call with any errors found.
+    """
+    required = {}  # A map of header name to line_number and the template entity.
+        # Example of required: { '<functional>': (1219, 'less<>') }
+
+    for line_number in xrange(clean_lines.num_lines()):
+        line = clean_lines.elided[line_number]
+        if not line or line[0] == '#':
+            continue
+
+        # String is special -- it is a non-templatized type in STL.
+        if _RE_PATTERN_STRING.search(line):
+            required['<string>'] = (line_number, 'string')
+
+        for pattern, template, header in _re_pattern_algorithm_header:
+            if pattern.search(line):
+                required[header] = (line_number, template)
+
+        # The following function is just a speed up, no semantics are changed.
+        if not '<' in line:  # Reduces the cpu time usage by skipping lines.
+            continue
+
+        for pattern, template, header in _re_pattern_templates:
+            if pattern.search(line):
+                required[header] = (line_number, template)
+
+    # The policy is that if you #include something in foo.h you don't need to
+    # include it again in foo.cpp. Here, we will look at possible includes.
+    # Let's copy the include_state so it is only messed up within this function.
+    include_state = include_state.copy()
+
+    # Did we find the header for this file (if any) and succesfully load it?
+    header_found = False
+
+    # Use the absolute path so that matching works properly.
+    abs_filename = os.path.abspath(filename)
+
+    # For Emacs's flymake.
+    # If cpp_style is invoked from Emacs's flymake, a temporary file is generated
+    # by flymake and that file name might end with '_flymake.cpp'. In that case,
+    # restore original file name here so that the corresponding header file can be
+    # found.
+    # e.g. If the file name is 'foo_flymake.cpp', we should search for 'foo.h'
+    # instead of 'foo_flymake.h'
+    abs_filename = re.sub(r'_flymake\.cpp$', '.cpp', abs_filename)
+
+    # include_state is modified during iteration, so we iterate over a copy of
+    # the keys.
+    for header in include_state.keys():  #NOLINT
+        (same_module, common_path) = files_belong_to_same_module(abs_filename, header)
+        fullpath = common_path + header
+        if same_module and update_include_state(fullpath, include_state):
+            header_found = True
+
+    # If we can't find the header file for a .cpp, assume it's because we don't
+    # know where to look. In that case we'll give up as we're not sure they
+    # didn't include it in the .h file.
+    # FIXME: Do a better job of finding .h files so we are confident that
+    #        not having the .h file means there isn't one.
+    if filename.endswith('.cpp') and not header_found:
+        return
+
+    # All the lines have been processed, report the errors found.
+    for required_header_unstripped in required:
+        template = required[required_header_unstripped][1]
+        if template in _HEADERS_ACCEPTED_BUT_NOT_PROMOTED:
+            headers = _HEADERS_ACCEPTED_BUT_NOT_PROMOTED[template]
+            if [True for header in headers if header in include_state]:
+                continue
+        if required_header_unstripped.strip('<>"') not in include_state:
+            error(required[required_header_unstripped][0],
+                  'build/include_what_you_use', 4,
+                  'Add #include ' + required_header_unstripped + ' for ' + template)
+
+
+def process_line(filename, file_extension,
+                 clean_lines, line, include_state, function_state,
+                 class_state, file_state, error):
+    """Processes a single line in the file.
+
+    Args:
+      filename: Filename of the file that is being processed.
+      file_extension: The extension (dot not included) of the file.
+      clean_lines: An array of strings, each representing a line of the file,
+                   with comments stripped.
+      line: Number of line being processed.
+      include_state: An _IncludeState instance in which the headers are inserted.
+      function_state: A _FunctionState instance which counts function lines, etc.
+      class_state: A _ClassState instance which maintains information about
+                   the current stack of nested class declarations being parsed.
+      file_state: A _FileState instance which maintains information about
+                  the state of things in the file.
+      error: A callable to which errors are reported, which takes arguments:
+             line number, error level, and message
+
+    """
+    raw_lines = clean_lines.raw_lines
+    detect_functions(clean_lines, line, function_state, error)
+    check_for_function_lengths(clean_lines, line, function_state, error)
+    if search(r'\bNOLINT\b', raw_lines[line]):  # ignore nolint lines
+        return
+    if match(r'\s*\b__asm\b', raw_lines[line]):  # Ignore asm lines as they format differently.
+        return
+    check_function_definition(filename, file_extension, clean_lines, line, function_state, error)
+    check_pass_ptr_usage(clean_lines, line, function_state, error)
+    check_for_leaky_patterns(clean_lines, line, function_state, error)
+    check_for_multiline_comments_and_strings(clean_lines, line, error)
+    check_style(clean_lines, line, file_extension, class_state, file_state, error)
+    check_language(filename, clean_lines, line, file_extension, include_state,
+                   file_state, error)
+    check_for_non_standard_constructs(clean_lines, line, class_state, error)
+    check_posix_threading(clean_lines, line, error)
+    check_invalid_increment(clean_lines, line, error)
+
+
+def _process_lines(filename, file_extension, lines, error, min_confidence):
+    """Performs lint checks and reports any errors to the given error function.
+
+    Args:
+      filename: Filename of the file that is being processed.
+      file_extension: The extension (dot not included) of the file.
+      lines: An array of strings, each representing a line of the file, with the
+             last element being empty if the file is termined with a newline.
+      error: A callable to which errors are reported, which takes 4 arguments:
+    """
+    lines = (['// marker so line numbers and indices both start at 1'] + lines +
+             ['// marker so line numbers end in a known way'])
+
+    include_state = _IncludeState()
+    function_state = _FunctionState(min_confidence)
+    class_state = _ClassState()
+
+    check_for_copyright(lines, error)
+
+    if file_extension == 'h':
+        check_for_header_guard(filename, lines, error)
+
+    remove_multi_line_comments(lines, error)
+    clean_lines = CleansedLines(lines)
+    file_state = _FileState(clean_lines, file_extension)
+    for line in xrange(clean_lines.num_lines()):
+        process_line(filename, file_extension, clean_lines, line,
+                     include_state, function_state, class_state, file_state, error)
+    class_state.check_finished(error)
+
+    check_for_include_what_you_use(filename, clean_lines, include_state, error)
+
+    # We check here rather than inside process_line so that we see raw
+    # lines rather than "cleaned" lines.
+    check_for_unicode_replacement_characters(lines, error)
+
+    check_for_new_line_at_eof(lines, error)
+
+
+class CppChecker(object):
+
+    """Processes C++ lines for checking style."""
+
+    # This list is used to--
+    #
+    # (1) generate an explicit list of all possible categories,
+    # (2) unit test that all checked categories have valid names, and
+    # (3) unit test that all categories are getting unit tested.
+    #
+    categories = set([
+        'build/class',
+        'build/deprecated',
+        'build/endif_comment',
+        'build/forward_decl',
+        'build/header_guard',
+        'build/include',
+        'build/include_order',
+        'build/include_what_you_use',
+        'build/namespaces',
+        'build/printf_format',
+        'build/storage_class',
+        'build/using_std',
+        'legal/copyright',
+        'readability/braces',
+        'readability/casting',
+        'readability/check',
+        'readability/comparison_to_zero',
+        'readability/constructors',
+        'readability/control_flow',
+        'readability/fn_size',
+        'readability/function',
+        'readability/multiline_comment',
+        'readability/multiline_string',
+        'readability/parameter_name',
+        'readability/naming',
+        'readability/naming/underscores',
+        'readability/null',
+        'readability/pass_ptr',
+        'readability/streams',
+        'readability/todo',
+        'readability/utf8',
+        'readability/webkit_export',
+        'runtime/arrays',
+        'runtime/bitfields',
+        'runtime/casting',
+        'runtime/ctype_function',
+        'runtime/explicit',
+        'runtime/init',
+        'runtime/int',
+        'runtime/invalid_increment',
+        'runtime/leaky_pattern',
+        'runtime/max_min_macros',
+        'runtime/memset',
+        'runtime/printf',
+        'runtime/printf_format',
+        'runtime/references',
+        'runtime/rtti',
+        'runtime/sizeof',
+        'runtime/string',
+        'runtime/threadsafe_fn',
+        'runtime/unsigned',
+        'runtime/virtual',
+        'whitespace/blank_line',
+        'whitespace/braces',
+        'whitespace/comma',
+        'whitespace/comments',
+        'whitespace/declaration',
+        'whitespace/end_of_line',
+        'whitespace/ending_newline',
+        'whitespace/indent',
+        'whitespace/line_length',
+        'whitespace/newline',
+        'whitespace/operators',
+        'whitespace/parens',
+        'whitespace/semicolon',
+        'whitespace/tab',
+        'whitespace/todo',
+        ])
+
+    def __init__(self, file_path, file_extension, handle_style_error,
+                 min_confidence):
+        """Create a CppChecker instance.
+
+        Args:
+          file_extension: A string that is the file extension, without
+                          the leading dot.
+
+        """
+        self.file_extension = file_extension
+        self.file_path = file_path
+        self.handle_style_error = handle_style_error
+        self.min_confidence = min_confidence
+
+    # Useful for unit testing.
+    def __eq__(self, other):
+        """Return whether this CppChecker instance is equal to another."""
+        if self.file_extension != other.file_extension:
+            return False
+        if self.file_path != other.file_path:
+            return False
+        if self.handle_style_error != other.handle_style_error:
+            return False
+        if self.min_confidence != other.min_confidence:
+            return False
+
+        return True
+
+    # Useful for unit testing.
+    def __ne__(self, other):
+        # Python does not automatically deduce __ne__() from __eq__().
+        return not self.__eq__(other)
+
+    def check(self, lines):
+        _process_lines(self.file_path, self.file_extension, lines,
+                       self.handle_style_error, self.min_confidence)
+
+
+# FIXME: Remove this function (requires refactoring unit tests).
+def process_file_data(filename, file_extension, lines, error, min_confidence, unit_test_config):
+    global _unit_test_config
+    _unit_test_config = unit_test_config
+    checker = CppChecker(filename, file_extension, error, min_confidence)
+    checker.check(lines)
+    _unit_test_config = {}
diff --git a/Tools/Scripts/webkitpy/style/checkers/cpp_unittest.py b/Tools/Scripts/webkitpy/style/checkers/cpp_unittest.py
new file mode 100644
index 0000000..5522201
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/cpp_unittest.py
@@ -0,0 +1,4866 @@
+#!/usr/bin/python
+# -*- coding: utf-8; -*-
+#
+# Copyright (C) 2011 Google Inc. All rights reserved.
+# Copyright (C) 2009 Torch Mobile Inc.
+# Copyright (C) 2009 Apple Inc. All rights reserved.
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+"""Unit test for cpp_style.py."""
+
+# FIXME: Add a good test that tests UpdateIncludeState.
+
+import codecs
+import os
+import random
+import re
+import unittest
+import cpp as cpp_style
+from cpp import CppChecker
+from ..filter import FilterConfiguration
+
+# This class works as an error collector and replaces cpp_style.Error
+# function for the unit tests.  We also verify each category we see
+# is in STYLE_CATEGORIES, to help keep that list up to date.
+class ErrorCollector:
+    _all_style_categories = CppChecker.categories
+    # This is a list including all categories seen in any unit test.
+    _seen_style_categories = {}
+
+    def __init__(self, assert_fn, filter=None, lines_to_check=None):
+        """assert_fn: a function to call when we notice a problem.
+           filter: filters the errors that we are concerned about."""
+        self._assert_fn = assert_fn
+        self._errors = []
+        self._lines_to_check = lines_to_check
+        if not filter:
+            filter = FilterConfiguration()
+        self._filter = filter
+
+    def __call__(self, line_number, category, confidence, message):
+        self._assert_fn(category in self._all_style_categories,
+                        'Message "%s" has category "%s",'
+                        ' which is not in STYLE_CATEGORIES' % (message, category))
+
+        if self._lines_to_check and not line_number in self._lines_to_check:
+            return False
+
+        if self._filter.should_check(category, ""):
+            self._seen_style_categories[category] = 1
+            self._errors.append('%s  [%s] [%d]' % (message, category, confidence))
+        return True
+
+    def results(self):
+        if len(self._errors) < 2:
+            return ''.join(self._errors)  # Most tests expect to have a string.
+        else:
+            return self._errors  # Let's give a list if there is more than one.
+
+    def result_list(self):
+        return self._errors
+
+    def verify_all_categories_are_seen(self):
+        """Fails if there's a category in _all_style_categories - _seen_style_categories.
+
+        This should only be called after all tests are run, so
+        _seen_style_categories has had a chance to fully populate.  Since
+        this isn't called from within the normal unittest framework, we
+        can't use the normal unittest assert macros.  Instead we just exit
+        when we see an error.  Good thing this test is always run last!
+        """
+        for category in self._all_style_categories:
+            if category not in self._seen_style_categories:
+                import sys
+                sys.exit('FATAL ERROR: There are no tests for category "%s"' % category)
+
+
+# This class is a lame mock of codecs. We do not verify filename, mode, or
+# encoding, but for the current use case it is not needed.
+class MockIo:
+    def __init__(self, mock_file):
+        self.mock_file = mock_file
+
+    def open(self, unused_filename, unused_mode, unused_encoding, _):  # NOLINT
+        # (lint doesn't like open as a method name)
+        return self.mock_file
+
+
+class CppFunctionsTest(unittest.TestCase):
+
+    """Supports testing functions that do not need CppStyleTestBase."""
+
+    def test_convert_to_lower_with_underscores(self):
+        self.assertEquals(cpp_style._convert_to_lower_with_underscores('ABC'), 'abc')
+        self.assertEquals(cpp_style._convert_to_lower_with_underscores('aB'), 'a_b')
+        self.assertEquals(cpp_style._convert_to_lower_with_underscores('isAName'), 'is_a_name')
+        self.assertEquals(cpp_style._convert_to_lower_with_underscores('AnotherTest'), 'another_test')
+        self.assertEquals(cpp_style._convert_to_lower_with_underscores('PassRefPtr<MyClass>'), 'pass_ref_ptr<my_class>')
+        self.assertEquals(cpp_style._convert_to_lower_with_underscores('_ABC'), '_abc')
+
+    def test_create_acronym(self):
+        self.assertEquals(cpp_style._create_acronym('ABC'), 'ABC')
+        self.assertEquals(cpp_style._create_acronym('IsAName'), 'IAN')
+        self.assertEquals(cpp_style._create_acronym('PassRefPtr<MyClass>'), 'PRP<MC>')
+
+    def test_is_c_or_objective_c(self):
+        clean_lines = cpp_style.CleansedLines([''])
+        clean_objc_lines = cpp_style.CleansedLines(['#import "header.h"'])
+        self.assertTrue(cpp_style._FileState(clean_lines, 'c').is_c_or_objective_c())
+        self.assertTrue(cpp_style._FileState(clean_lines, 'm').is_c_or_objective_c())
+        self.assertFalse(cpp_style._FileState(clean_lines, 'cpp').is_c_or_objective_c())
+        self.assertFalse(cpp_style._FileState(clean_lines, 'cc').is_c_or_objective_c())
+        self.assertFalse(cpp_style._FileState(clean_lines, 'h').is_c_or_objective_c())
+        self.assertTrue(cpp_style._FileState(clean_objc_lines, 'h').is_c_or_objective_c())
+
+    def test_parameter(self):
+        # Test type.
+        parameter = cpp_style.Parameter('ExceptionCode', 13, 1)
+        self.assertEquals(parameter.type, 'ExceptionCode')
+        self.assertEquals(parameter.name, '')
+        self.assertEquals(parameter.row, 1)
+
+        # Test type and name.
+        parameter = cpp_style.Parameter('PassRefPtr<MyClass> parent', 19, 1)
+        self.assertEquals(parameter.type, 'PassRefPtr<MyClass>')
+        self.assertEquals(parameter.name, 'parent')
+        self.assertEquals(parameter.row, 1)
+
+        # Test type, no name, with default value.
+        parameter = cpp_style.Parameter('MyClass = 0', 7, 0)
+        self.assertEquals(parameter.type, 'MyClass')
+        self.assertEquals(parameter.name, '')
+        self.assertEquals(parameter.row, 0)
+
+        # Test type, name, and default value.
+        parameter = cpp_style.Parameter('MyClass a = 0', 7, 0)
+        self.assertEquals(parameter.type, 'MyClass')
+        self.assertEquals(parameter.name, 'a')
+        self.assertEquals(parameter.row, 0)
+
+    def test_single_line_view(self):
+        start_position = cpp_style.Position(row=1, column=1)
+        end_position = cpp_style.Position(row=3, column=1)
+        single_line_view = cpp_style.SingleLineView(['0', 'abcde', 'fgh', 'i'], start_position, end_position)
+        self.assertEquals(single_line_view.single_line, 'bcde fgh i')
+        self.assertEquals(single_line_view.convert_column_to_row(0), 1)
+        self.assertEquals(single_line_view.convert_column_to_row(4), 1)
+        self.assertEquals(single_line_view.convert_column_to_row(5), 2)
+        self.assertEquals(single_line_view.convert_column_to_row(8), 2)
+        self.assertEquals(single_line_view.convert_column_to_row(9), 3)
+        self.assertEquals(single_line_view.convert_column_to_row(100), 3)
+
+        start_position = cpp_style.Position(row=0, column=3)
+        end_position = cpp_style.Position(row=0, column=4)
+        single_line_view = cpp_style.SingleLineView(['abcdef'], start_position, end_position)
+        self.assertEquals(single_line_view.single_line, 'd')
+
+    def test_create_skeleton_parameters(self):
+        self.assertEquals(cpp_style.create_skeleton_parameters(''), '')
+        self.assertEquals(cpp_style.create_skeleton_parameters(' '), ' ')
+        self.assertEquals(cpp_style.create_skeleton_parameters('long'), 'long,')
+        self.assertEquals(cpp_style.create_skeleton_parameters('const unsigned long int'), '                    int,')
+        self.assertEquals(cpp_style.create_skeleton_parameters('long int*'), '     int ,')
+        self.assertEquals(cpp_style.create_skeleton_parameters('PassRefPtr<Foo> a'), 'PassRefPtr      a,')
+        self.assertEquals(cpp_style.create_skeleton_parameters(
+                'ComplexTemplate<NestedTemplate1<MyClass1, MyClass2>, NestedTemplate1<MyClass1, MyClass2> > param, int second'),
+                          'ComplexTemplate                                                                            param, int second,')
+        self.assertEquals(cpp_style.create_skeleton_parameters('int = 0, Namespace::Type& a'), 'int    ,            Type  a,')
+        # Create skeleton parameters is a bit too aggressive with function variables, but
+        # it allows for parsing other parameters and declarations like this are rare.
+        self.assertEquals(cpp_style.create_skeleton_parameters('void (*fn)(int a, int b), Namespace::Type& a'),
+                          'void                    ,            Type  a,')
+
+        # This doesn't look like functions declarations but the simplifications help to eliminate false positives.
+        self.assertEquals(cpp_style.create_skeleton_parameters('b{d}'), 'b   ,')
+
+    def test_find_parameter_name_index(self):
+        self.assertEquals(cpp_style.find_parameter_name_index(' int a '), 5)
+        self.assertEquals(cpp_style.find_parameter_name_index(' PassRefPtr     '), 16)
+        self.assertEquals(cpp_style.find_parameter_name_index('double'), 6)
+
+    def test_parameter_list(self):
+        elided_lines = ['int blah(PassRefPtr<MyClass> paramName,',
+                        'const Other1Class& foo,',
+                        'const ComplexTemplate<Class1, NestedTemplate<P1, P2> >* const * param = new ComplexTemplate<Class1, NestedTemplate<P1, P2> >(34, 42),',
+                        'int* myCount = 0);']
+        start_position = cpp_style.Position(row=0, column=8)
+        end_position = cpp_style.Position(row=3, column=16)
+
+        expected_parameters = ({'type': 'PassRefPtr<MyClass>', 'name': 'paramName', 'row': 0},
+                               {'type': 'const Other1Class&', 'name': 'foo', 'row': 1},
+                               {'type': 'const ComplexTemplate<Class1, NestedTemplate<P1, P2> >* const *', 'name': 'param', 'row': 2},
+                               {'type': 'int*', 'name': 'myCount', 'row': 3})
+        index = 0
+        for parameter in cpp_style.parameter_list(elided_lines, start_position, end_position):
+            expected_parameter = expected_parameters[index]
+            self.assertEquals(parameter.type, expected_parameter['type'])
+            self.assertEquals(parameter.name, expected_parameter['name'])
+            self.assertEquals(parameter.row, expected_parameter['row'])
+            index += 1
+        self.assertEquals(index, len(expected_parameters))
+
+    def test_check_parameter_against_text(self):
+        error_collector = ErrorCollector(self.assert_)
+        parameter = cpp_style.Parameter('FooF ooF', 4, 1)
+        self.assertFalse(cpp_style._check_parameter_name_against_text(parameter, 'FooF', error_collector))
+        self.assertEquals(error_collector.results(),
+                          'The parameter name "ooF" adds no information, so it should be removed.  [readability/parameter_name] [5]')
+
+class CppStyleTestBase(unittest.TestCase):
+    """Provides some useful helper functions for cpp_style tests.
+
+    Attributes:
+      min_confidence: An integer that is the current minimum confidence
+                      level for the tests.
+
+    """
+
+    # FIXME: Refactor the unit tests so the confidence level is passed
+    #        explicitly, just like it is in the real code.
+    min_confidence = 1;
+
+    # Helper function to avoid needing to explicitly pass confidence
+    # in all the unit test calls to cpp_style.process_file_data().
+    def process_file_data(self, filename, file_extension, lines, error, unit_test_config={}):
+        """Call cpp_style.process_file_data() with the min_confidence."""
+        return cpp_style.process_file_data(filename, file_extension, lines,
+                                           error, self.min_confidence, unit_test_config)
+
+    def perform_lint(self, code, filename, basic_error_rules, unit_test_config={}, lines_to_check=None):
+        error_collector = ErrorCollector(self.assert_, FilterConfiguration(basic_error_rules), lines_to_check)
+        lines = code.split('\n')
+        extension = filename.split('.')[1]
+        self.process_file_data(filename, extension, lines, error_collector, unit_test_config)
+        return error_collector.results()
+
+    # Perform lint on single line of input and return the error message.
+    def perform_single_line_lint(self, code, filename):
+        basic_error_rules = ('-build/header_guard',
+                             '-legal/copyright',
+                             '-readability/fn_size',
+                             '-readability/parameter_name',
+                             '-readability/pass_ptr',
+                             '-whitespace/ending_newline')
+        return self.perform_lint(code, filename, basic_error_rules)
+
+    # Perform lint over multiple lines and return the error message.
+    def perform_multi_line_lint(self, code, file_extension):
+        basic_error_rules = ('-build/header_guard',
+                             '-legal/copyright',
+                             '-readability/parameter_name',
+                             '-whitespace/ending_newline')
+        return self.perform_lint(code, 'test.' + file_extension, basic_error_rules)
+
+    # Only keep some errors related to includes, namespaces and rtti.
+    def perform_language_rules_check(self, filename, code, lines_to_check=None):
+        basic_error_rules = ('-',
+                             '+build/include',
+                             '+build/include_order',
+                             '+build/namespaces',
+                             '+runtime/rtti')
+        return self.perform_lint(code, filename, basic_error_rules, lines_to_check=lines_to_check)
+
+    # Only keep function length errors.
+    def perform_function_lengths_check(self, code):
+        basic_error_rules = ('-',
+                             '+readability/fn_size')
+        return self.perform_lint(code, 'test.cpp', basic_error_rules)
+
+    # Only keep pass ptr errors.
+    def perform_pass_ptr_check(self, code):
+        basic_error_rules = ('-',
+                             '+readability/pass_ptr')
+        return self.perform_lint(code, 'test.cpp', basic_error_rules)
+
+    # Only keep leaky pattern errors.
+    def perform_leaky_pattern_check(self, code):
+        basic_error_rules = ('-',
+                             '+runtime/leaky_pattern')
+        return self.perform_lint(code, 'test.cpp', basic_error_rules)
+
+    # Only include what you use errors.
+    def perform_include_what_you_use(self, code, filename='foo.h', io=codecs):
+        basic_error_rules = ('-',
+                             '+build/include_what_you_use')
+        unit_test_config = {cpp_style.INCLUDE_IO_INJECTION_KEY: io}
+        return self.perform_lint(code, filename, basic_error_rules, unit_test_config)
+
+    # Perform lint and compare the error message with "expected_message".
+    def assert_lint(self, code, expected_message, file_name='foo.cpp'):
+        self.assertEquals(expected_message, self.perform_single_line_lint(code, file_name))
+
+    def assert_lint_one_of_many_errors_re(self, code, expected_message_re, file_name='foo.cpp'):
+        messages = self.perform_single_line_lint(code, file_name)
+        for message in messages:
+            if re.search(expected_message_re, message):
+                return
+
+        self.assertEquals(expected_message_re, messages)
+
+    def assert_multi_line_lint(self, code, expected_message, file_name='foo.h'):
+        file_extension = file_name[file_name.rfind('.') + 1:]
+        self.assertEquals(expected_message, self.perform_multi_line_lint(code, file_extension))
+
+    def assert_multi_line_lint_re(self, code, expected_message_re, file_name='foo.h'):
+        file_extension = file_name[file_name.rfind('.') + 1:]
+        message = self.perform_multi_line_lint(code, file_extension)
+        if not re.search(expected_message_re, message):
+            self.fail('Message was:\n' + message + 'Expected match to "' + expected_message_re + '"')
+
+    def assert_language_rules_check(self, file_name, code, expected_message, lines_to_check=None):
+        self.assertEquals(expected_message,
+                          self.perform_language_rules_check(file_name, code, lines_to_check))
+
+    def assert_include_what_you_use(self, code, expected_message):
+        self.assertEquals(expected_message,
+                          self.perform_include_what_you_use(code))
+
+    def assert_blank_lines_check(self, lines, start_errors, end_errors):
+        error_collector = ErrorCollector(self.assert_)
+        self.process_file_data('foo.cpp', 'cpp', lines, error_collector)
+        self.assertEquals(
+            start_errors,
+            error_collector.results().count(
+                'Blank line at the start of a code block.  Is this needed?'
+                '  [whitespace/blank_line] [2]'))
+        self.assertEquals(
+            end_errors,
+            error_collector.results().count(
+                'Blank line at the end of a code block.  Is this needed?'
+                '  [whitespace/blank_line] [3]'))
+
+    def assert_positions_equal(self, position, tuple_position):
+        """Checks if the two positions are equal.
+
+        position: a cpp_style.Position object.
+        tuple_position: a tuple (row, column) to compare against."""
+        self.assertEquals(position, cpp_style.Position(tuple_position[0], tuple_position[1]),
+                          'position %s, tuple_position %s' % (position, tuple_position))
+
+
+class FunctionDetectionTest(CppStyleTestBase):
+    def perform_function_detection(self, lines, function_information, detection_line=0):
+        clean_lines = cpp_style.CleansedLines(lines)
+        function_state = cpp_style._FunctionState(5)
+        error_collector = ErrorCollector(self.assert_)
+        cpp_style.detect_functions(clean_lines, detection_line, function_state, error_collector)
+        if not function_information:
+            self.assertEquals(function_state.in_a_function, False)
+            return
+        self.assertEquals(function_state.in_a_function, True)
+        self.assertEquals(function_state.current_function, function_information['name'] + '()')
+        self.assertEquals(function_state.modifiers_and_return_type(), function_information['modifiers_and_return_type'])
+        self.assertEquals(function_state.is_pure, function_information['is_pure'])
+        self.assertEquals(function_state.is_declaration, function_information['is_declaration'])
+        self.assert_positions_equal(function_state.function_name_start_position, function_information['function_name_start_position'])
+        self.assert_positions_equal(function_state.parameter_start_position, function_information['parameter_start_position'])
+        self.assert_positions_equal(function_state.parameter_end_position, function_information['parameter_end_position'])
+        self.assert_positions_equal(function_state.body_start_position, function_information['body_start_position'])
+        self.assert_positions_equal(function_state.end_position, function_information['end_position'])
+        expected_parameters = function_information.get('parameter_list')
+        if expected_parameters:
+            actual_parameters = function_state.parameter_list()
+            self.assertEquals(len(actual_parameters), len(expected_parameters))
+            for index in range(len(expected_parameters)):
+                actual_parameter = actual_parameters[index]
+                expected_parameter = expected_parameters[index]
+                self.assertEquals(actual_parameter.type, expected_parameter['type'])
+                self.assertEquals(actual_parameter.name, expected_parameter['name'])
+                self.assertEquals(actual_parameter.row, expected_parameter['row'])
+
+    def test_basic_function_detection(self):
+        self.perform_function_detection(
+            ['void theTestFunctionName(int) {',
+             '}'],
+            {'name': 'theTestFunctionName',
+             'modifiers_and_return_type': 'void',
+             'function_name_start_position': (0, 5),
+             'parameter_start_position': (0, 24),
+             'parameter_end_position': (0, 29),
+             'body_start_position': (0, 30),
+             'end_position': (1, 1),
+             'is_pure': False,
+             'is_declaration': False})
+
+    def test_function_declaration_detection(self):
+        self.perform_function_detection(
+            ['void aFunctionName(int);'],
+            {'name': 'aFunctionName',
+             'modifiers_and_return_type': 'void',
+             'function_name_start_position': (0, 5),
+             'parameter_start_position': (0, 18),
+             'parameter_end_position': (0, 23),
+             'body_start_position': (0, 23),
+             'end_position': (0, 24),
+             'is_pure': False,
+             'is_declaration': True})
+
+        self.perform_function_detection(
+            ['CheckedInt<T> operator /(const CheckedInt<T> &lhs, const CheckedInt<T> &rhs);'],
+            {'name': 'operator /',
+             'modifiers_and_return_type': 'CheckedInt<T>',
+             'function_name_start_position': (0, 14),
+             'parameter_start_position': (0, 24),
+             'parameter_end_position': (0, 76),
+             'body_start_position': (0, 76),
+             'end_position': (0, 77),
+             'is_pure': False,
+             'is_declaration': True})
+
+        self.perform_function_detection(
+            ['CheckedInt<T> operator -(const CheckedInt<T> &lhs, const CheckedInt<T> &rhs);'],
+            {'name': 'operator -',
+             'modifiers_and_return_type': 'CheckedInt<T>',
+             'function_name_start_position': (0, 14),
+             'parameter_start_position': (0, 24),
+             'parameter_end_position': (0, 76),
+             'body_start_position': (0, 76),
+             'end_position': (0, 77),
+             'is_pure': False,
+             'is_declaration': True})
+
+        self.perform_function_detection(
+            ['CheckedInt<T> operator !=(const CheckedInt<T> &lhs, const CheckedInt<T> &rhs);'],
+            {'name': 'operator !=',
+             'modifiers_and_return_type': 'CheckedInt<T>',
+             'function_name_start_position': (0, 14),
+             'parameter_start_position': (0, 25),
+             'parameter_end_position': (0, 77),
+             'body_start_position': (0, 77),
+             'end_position': (0, 78),
+             'is_pure': False,
+             'is_declaration': True})
+
+        self.perform_function_detection(
+            ['CheckedInt<T> operator +(const CheckedInt<T> &lhs, const CheckedInt<T> &rhs);'],
+            {'name': 'operator +',
+             'modifiers_and_return_type': 'CheckedInt<T>',
+             'function_name_start_position': (0, 14),
+             'parameter_start_position': (0, 24),
+             'parameter_end_position': (0, 76),
+             'body_start_position': (0, 76),
+             'end_position': (0, 77),
+             'is_pure': False,
+             'is_declaration': True})
+
+    def test_pure_function_detection(self):
+        self.perform_function_detection(
+            ['virtual void theTestFunctionName(int = 0);'],
+            {'name': 'theTestFunctionName',
+             'modifiers_and_return_type': 'virtual void',
+             'function_name_start_position': (0, 13),
+             'parameter_start_position': (0, 32),
+             'parameter_end_position': (0, 41),
+             'body_start_position': (0, 41),
+             'end_position': (0, 42),
+             'is_pure': False,
+             'is_declaration': True})
+
+        self.perform_function_detection(
+            ['virtual void theTestFunctionName(int) = 0;'],
+            {'name': 'theTestFunctionName',
+             'modifiers_and_return_type': 'virtual void',
+             'function_name_start_position': (0, 13),
+             'parameter_start_position': (0, 32),
+             'parameter_end_position': (0, 37),
+             'body_start_position': (0, 41),
+             'end_position': (0, 42),
+             'is_pure': True,
+             'is_declaration': True})
+
+        # Hopefully, no one writes code like this but it is a tricky case.
+        self.perform_function_detection(
+            ['virtual void theTestFunctionName(int)',
+             ' = ',
+             ' 0 ;'],
+            {'name': 'theTestFunctionName',
+             'modifiers_and_return_type': 'virtual void',
+             'function_name_start_position': (0, 13),
+             'parameter_start_position': (0, 32),
+             'parameter_end_position': (0, 37),
+             'body_start_position': (2, 3),
+             'end_position': (2, 4),
+             'is_pure': True,
+             'is_declaration': True})
+
+    def test_ignore_macros(self):
+        self.perform_function_detection(['void aFunctionName(int); \\'], None)
+
+    def test_non_functions(self):
+        # This case exposed an error because the open brace was in quotes.
+        self.perform_function_detection(
+            ['asm(',
+             '    "stmdb sp!, {r1-r3}" "\n"',
+             ');'],
+            # This isn't a function but it looks like one to our simple
+            # algorithm and that is ok.
+            {'name': 'asm',
+             'modifiers_and_return_type': '',
+             'function_name_start_position': (0, 0),
+             'parameter_start_position': (0, 3),
+             'parameter_end_position': (2, 1),
+             'body_start_position': (2, 1),
+             'end_position': (2, 2),
+             'is_pure': False,
+             'is_declaration': True})
+
+        # Simple test case with something that is not a function.
+        self.perform_function_detection(['class Stuff;'], None)
+
+    def test_parameter_list(self):
+        # A function with no arguments.
+        function_state = self.perform_function_detection(
+            ['void functionName();'],
+            {'name': 'functionName',
+             'modifiers_and_return_type': 'void',
+             'function_name_start_position': (0, 5),
+             'parameter_start_position': (0, 17),
+             'parameter_end_position': (0, 19),
+             'body_start_position': (0, 19),
+             'end_position': (0, 20),
+             'is_pure': False,
+             'is_declaration': True,
+             'parameter_list': ()})
+
+        # A function with one argument.
+        function_state = self.perform_function_detection(
+            ['void functionName(int);'],
+            {'name': 'functionName',
+             'modifiers_and_return_type': 'void',
+             'function_name_start_position': (0, 5),
+             'parameter_start_position': (0, 17),
+             'parameter_end_position': (0, 22),
+             'body_start_position': (0, 22),
+             'end_position': (0, 23),
+             'is_pure': False,
+             'is_declaration': True,
+             'parameter_list':
+                 ({'type': 'int', 'name': '', 'row': 0},)})
+
+        # A function with unsigned and short arguments
+        function_state = self.perform_function_detection(
+            ['void functionName(unsigned a, short b, long c, long long short unsigned int);'],
+            {'name': 'functionName',
+             'modifiers_and_return_type': 'void',
+             'function_name_start_position': (0, 5),
+             'parameter_start_position': (0, 17),
+             'parameter_end_position': (0, 76),
+             'body_start_position': (0, 76),
+             'end_position': (0, 77),
+             'is_pure': False,
+             'is_declaration': True,
+             'parameter_list':
+                 ({'type': 'unsigned', 'name': 'a', 'row': 0},
+                  {'type': 'short', 'name': 'b', 'row': 0},
+                  {'type': 'long', 'name': 'c', 'row': 0},
+                  {'type': 'long long short unsigned int', 'name': '', 'row': 0})})
+
+        # Some parameter type with modifiers and no parameter names.
+        function_state = self.perform_function_detection(
+            ['virtual void determineARIADropEffects(Vector<String>*&, const unsigned long int*&, const MediaPlayer::Preload, Other<Other2, Other3<P1, P2> >, int);'],
+            {'name': 'determineARIADropEffects',
+             'modifiers_and_return_type': 'virtual void',
+             'parameter_start_position': (0, 37),
+             'function_name_start_position': (0, 13),
+             'parameter_end_position': (0, 147),
+             'body_start_position': (0, 147),
+             'end_position': (0, 148),
+             'is_pure': False,
+             'is_declaration': True,
+             'parameter_list':
+                 ({'type': 'Vector<String>*&', 'name': '', 'row': 0},
+                  {'type': 'const unsigned long int*&', 'name': '', 'row': 0},
+                  {'type': 'const MediaPlayer::Preload', 'name': '', 'row': 0},
+                  {'type': 'Other<Other2, Other3<P1, P2> >', 'name': '', 'row': 0},
+                  {'type': 'int', 'name': '', 'row': 0})})
+
+        # Try parsing a function with a very complex definition.
+        function_state = self.perform_function_detection(
+            ['#define MyMacro(a) a',
+             'virtual',
+             'AnotherTemplate<Class1, Class2> aFunctionName(PassRefPtr<MyClass> paramName,',
+             'const Other1Class& foo,',
+             'const ComplexTemplate<Class1, NestedTemplate<P1, P2> >* const * param = new ComplexTemplate<Class1, NestedTemplate<P1, P2> >(34, 42),',
+             'int* myCount = 0);'],
+            {'name': 'aFunctionName',
+             'modifiers_and_return_type': 'virtual AnotherTemplate<Class1, Class2>',
+             'function_name_start_position': (2, 32),
+             'parameter_start_position': (2, 45),
+             'parameter_end_position': (5, 17),
+             'body_start_position': (5, 17),
+             'end_position': (5, 18),
+             'is_pure': False,
+             'is_declaration': True,
+             'parameter_list':
+                 ({'type': 'PassRefPtr<MyClass>', 'name': 'paramName', 'row': 2},
+                  {'type': 'const Other1Class&', 'name': 'foo', 'row': 3},
+                  {'type': 'const ComplexTemplate<Class1, NestedTemplate<P1, P2> >* const *', 'name': 'param', 'row': 4},
+                  {'type': 'int*', 'name': 'myCount', 'row': 5})},
+            detection_line=2)
+
+
+class CppStyleTest(CppStyleTestBase):
+
+    def test_asm_lines_ignored(self):
+        self.assert_lint(
+            '__asm mov [registration], eax',
+            '')
+
+    # Test get line width.
+    def test_get_line_width(self):
+        self.assertEquals(0, cpp_style.get_line_width(''))
+        self.assertEquals(10, cpp_style.get_line_width(u'x' * 10))
+        self.assertEquals(16, cpp_style.get_line_width(u'都|道|府|県|支庁'))
+
+    def test_find_next_multi_line_comment_start(self):
+        self.assertEquals(1, cpp_style.find_next_multi_line_comment_start([''], 0))
+
+        lines = ['a', 'b', '/* c']
+        self.assertEquals(2, cpp_style.find_next_multi_line_comment_start(lines, 0))
+
+        lines = ['char a[] = "/*";']  # not recognized as comment.
+        self.assertEquals(1, cpp_style.find_next_multi_line_comment_start(lines, 0))
+
+    def test_find_next_multi_line_comment_end(self):
+        self.assertEquals(1, cpp_style.find_next_multi_line_comment_end([''], 0))
+        lines = ['a', 'b', ' c */']
+        self.assertEquals(2, cpp_style.find_next_multi_line_comment_end(lines, 0))
+
+    def test_remove_multi_line_comments_from_range(self):
+        lines = ['a', '  /* comment ', ' * still comment', ' comment */   ', 'b']
+        cpp_style.remove_multi_line_comments_from_range(lines, 1, 4)
+        self.assertEquals(['a', '// dummy', '// dummy', '// dummy', 'b'], lines)
+
+    def test_position(self):
+        position = cpp_style.Position(3, 4)
+        self.assert_positions_equal(position, (3, 4))
+        self.assertEquals(position.row, 3)
+        self.assertTrue(position > cpp_style.Position(position.row - 1, position.column + 1))
+        self.assertTrue(position > cpp_style.Position(position.row, position.column - 1))
+        self.assertTrue(position < cpp_style.Position(position.row, position.column + 1))
+        self.assertTrue(position < cpp_style.Position(position.row + 1, position.column - 1))
+        self.assertEquals(position.__str__(), '(3, 4)')
+
+    def test_rfind_in_lines(self):
+        not_found_position = cpp_style.Position(10, 11)
+        start_position = cpp_style.Position(2, 2)
+        lines = ['ab', 'ace', 'test']
+        self.assertEquals(not_found_position, cpp_style._rfind_in_lines('st', lines, start_position, not_found_position))
+        self.assertTrue(cpp_style.Position(1, 1) == cpp_style._rfind_in_lines('a', lines, start_position, not_found_position))
+        self.assertEquals(cpp_style.Position(2, 2), cpp_style._rfind_in_lines('(te|a)', lines, start_position, not_found_position))
+
+    def test_close_expression(self):
+        self.assertEquals(cpp_style.Position(1, -1), cpp_style.close_expression([')('], cpp_style.Position(0, 1)))
+        self.assertEquals(cpp_style.Position(1, -1), cpp_style.close_expression([') ()'], cpp_style.Position(0, 1)))
+        self.assertEquals(cpp_style.Position(0, 4), cpp_style.close_expression([')[)]'], cpp_style.Position(0, 1)))
+        self.assertEquals(cpp_style.Position(0, 5), cpp_style.close_expression(['}{}{}'], cpp_style.Position(0, 3)))
+        self.assertEquals(cpp_style.Position(1, 1), cpp_style.close_expression(['}{}{', '}'], cpp_style.Position(0, 3)))
+        self.assertEquals(cpp_style.Position(2, -1), cpp_style.close_expression(['][][', ' '], cpp_style.Position(0, 3)))
+
+    def test_spaces_at_end_of_line(self):
+        self.assert_lint(
+            '// Hello there ',
+            'Line ends in whitespace.  Consider deleting these extra spaces.'
+            '  [whitespace/end_of_line] [4]')
+
+    # Test C-style cast cases.
+    def test_cstyle_cast(self):
+        self.assert_lint(
+            'int a = (int)1.0;',
+            'Using C-style cast.  Use static_cast<int>(...) instead'
+            '  [readability/casting] [4]')
+        self.assert_lint(
+            'int *a = (int *)DEFINED_VALUE;',
+            'Using C-style cast.  Use reinterpret_cast<int *>(...) instead'
+            '  [readability/casting] [4]', 'foo.c')
+        self.assert_lint(
+            'uint16 a = (uint16)1.0;',
+            'Using C-style cast.  Use static_cast<uint16>(...) instead'
+            '  [readability/casting] [4]')
+        self.assert_lint(
+            'int32 a = (int32)1.0;',
+            'Using C-style cast.  Use static_cast<int32>(...) instead'
+            '  [readability/casting] [4]')
+        self.assert_lint(
+            'uint64 a = (uint64)1.0;',
+            'Using C-style cast.  Use static_cast<uint64>(...) instead'
+            '  [readability/casting] [4]')
+
+    # Test taking address of casts (runtime/casting)
+    def test_runtime_casting(self):
+        self.assert_lint(
+            'int* x = &static_cast<int*>(foo);',
+            'Are you taking an address of a cast?  '
+            'This is dangerous: could be a temp var.  '
+            'Take the address before doing the cast, rather than after'
+            '  [runtime/casting] [4]')
+
+        self.assert_lint(
+            'int* x = &dynamic_cast<int *>(foo);',
+            ['Are you taking an address of a cast?  '
+             'This is dangerous: could be a temp var.  '
+             'Take the address before doing the cast, rather than after'
+             '  [runtime/casting] [4]',
+             'Do not use dynamic_cast<>.  If you need to cast within a class '
+             'hierarchy, use static_cast<> to upcast.  Google doesn\'t support '
+             'RTTI.  [runtime/rtti] [5]'])
+
+        self.assert_lint(
+            'int* x = &reinterpret_cast<int *>(foo);',
+            'Are you taking an address of a cast?  '
+            'This is dangerous: could be a temp var.  '
+            'Take the address before doing the cast, rather than after'
+            '  [runtime/casting] [4]')
+
+        # It's OK to cast an address.
+        self.assert_lint(
+            'int* x = reinterpret_cast<int *>(&foo);',
+            '')
+
+    def test_runtime_selfinit(self):
+        self.assert_lint(
+            'Foo::Foo(Bar r, Bel l) : r_(r_), l_(l_) { }',
+            'You seem to be initializing a member variable with itself.'
+            '  [runtime/init] [4]')
+        self.assert_lint(
+            'Foo::Foo(Bar r, Bel l) : r_(r), l_(l) { }',
+            '')
+        self.assert_lint(
+            'Foo::Foo(Bar r) : r_(r), l_(r_), ll_(l_) { }',
+            '')
+
+    def test_runtime_rtti(self):
+        statement = 'int* x = dynamic_cast<int*>(&foo);'
+        error_message = (
+            'Do not use dynamic_cast<>.  If you need to cast within a class '
+            'hierarchy, use static_cast<> to upcast.  Google doesn\'t support '
+            'RTTI.  [runtime/rtti] [5]')
+        # dynamic_cast is disallowed in most files.
+        self.assert_language_rules_check('foo.cpp', statement, error_message)
+        self.assert_language_rules_check('foo.h', statement, error_message)
+
+    # Test for static_cast readability.
+    def test_static_cast_readability(self):
+        self.assert_lint(
+            'Text* x = static_cast<Text*>(foo);',
+            'Consider using toText helper function in WebCore/dom/Text.h '
+            'instead of static_cast<Text*>'
+            '  [readability/check] [4]')
+
+    # We cannot test this functionality because of difference of
+    # function definitions.  Anyway, we may never enable this.
+    #
+    # # Test for unnamed arguments in a method.
+    # def test_check_for_unnamed_params(self):
+    #   message = ('All parameters should be named in a function'
+    #              '  [readability/function] [3]')
+    #   self.assert_lint('virtual void A(int*) const;', message)
+    #   self.assert_lint('virtual void B(void (*fn)(int*));', message)
+    #   self.assert_lint('virtual void C(int*);', message)
+    #   self.assert_lint('void *(*f)(void *) = x;', message)
+    #   self.assert_lint('void Method(char*) {', message)
+    #   self.assert_lint('void Method(char*);', message)
+    #   self.assert_lint('void Method(char* /*x*/);', message)
+    #   self.assert_lint('typedef void (*Method)(int32);', message)
+    #   self.assert_lint('static void operator delete[](void*) throw();', message)
+    #
+    #   self.assert_lint('virtual void D(int* p);', '')
+    #   self.assert_lint('void operator delete(void* x) throw();', '')
+    #   self.assert_lint('void Method(char* x)\n{', '')
+    #   self.assert_lint('void Method(char* /*x*/)\n{', '')
+    #   self.assert_lint('void Method(char* x);', '')
+    #   self.assert_lint('typedef void (*Method)(int32 x);', '')
+    #   self.assert_lint('static void operator delete[](void* x) throw();', '')
+    #   self.assert_lint('static void operator delete[](void* /*x*/) throw();', '')
+    #
+    #   # This one should technically warn, but doesn't because the function
+    #   # pointer is confusing.
+    #   self.assert_lint('virtual void E(void (*fn)(int* p));', '')
+
+    # Test deprecated casts such as int(d)
+    def test_deprecated_cast(self):
+        self.assert_lint(
+            'int a = int(2.2);',
+            'Using deprecated casting style.  '
+            'Use static_cast<int>(...) instead'
+            '  [readability/casting] [4]')
+        # Checks for false positives...
+        self.assert_lint(
+            'int a = int(); // Constructor, o.k.',
+            '')
+        self.assert_lint(
+            'X::X() : a(int()) { } // default Constructor, o.k.',
+            '')
+        self.assert_lint(
+            'operator bool(); // Conversion operator, o.k.',
+            '')
+
+    # The second parameter to a gMock method definition is a function signature
+    # that often looks like a bad cast but should not picked up by lint.
+    def test_mock_method(self):
+        self.assert_lint(
+            'MOCK_METHOD0(method, int());',
+            '')
+        self.assert_lint(
+            'MOCK_CONST_METHOD1(method, float(string));',
+            '')
+        self.assert_lint(
+            'MOCK_CONST_METHOD2_T(method, double(float, float));',
+            '')
+
+    # Test sizeof(type) cases.
+    def test_sizeof_type(self):
+        self.assert_lint(
+            'sizeof(int);',
+            'Using sizeof(type).  Use sizeof(varname) instead if possible'
+            '  [runtime/sizeof] [1]')
+        self.assert_lint(
+            'sizeof(int *);',
+            'Using sizeof(type).  Use sizeof(varname) instead if possible'
+            '  [runtime/sizeof] [1]')
+
+    # Test typedef cases.  There was a bug that cpp_style misidentified
+    # typedef for pointer to function as C-style cast and produced
+    # false-positive error messages.
+    def test_typedef_for_pointer_to_function(self):
+        self.assert_lint(
+            'typedef void (*Func)(int x);',
+            '')
+        self.assert_lint(
+            'typedef void (*Func)(int *x);',
+            '')
+        self.assert_lint(
+            'typedef void Func(int x);',
+            '')
+        self.assert_lint(
+            'typedef void Func(int *x);',
+            '')
+
+    def test_include_what_you_use_no_implementation_files(self):
+        code = 'std::vector<int> foo;'
+        self.assertEquals('Add #include <vector> for vector<>'
+                          '  [build/include_what_you_use] [4]',
+                          self.perform_include_what_you_use(code, 'foo.h'))
+        self.assertEquals('',
+                          self.perform_include_what_you_use(code, 'foo.cpp'))
+
+    def test_include_what_you_use(self):
+        self.assert_include_what_you_use(
+            '''#include <vector>
+               std::vector<int> foo;
+            ''',
+            '')
+        self.assert_include_what_you_use(
+            '''#include <map>
+               std::pair<int,int> foo;
+            ''',
+            '')
+        self.assert_include_what_you_use(
+            '''#include <multimap>
+               std::pair<int,int> foo;
+            ''',
+            '')
+        self.assert_include_what_you_use(
+            '''#include <hash_map>
+               std::pair<int,int> foo;
+            ''',
+            '')
+        self.assert_include_what_you_use(
+            '''#include <utility>
+               std::pair<int,int> foo;
+            ''',
+            '')
+        self.assert_include_what_you_use(
+            '''#include <vector>
+               DECLARE_string(foobar);
+            ''',
+            '')
+        self.assert_include_what_you_use(
+            '''#include <vector>
+               DEFINE_string(foobar, "", "");
+            ''',
+            '')
+        self.assert_include_what_you_use(
+            '''#include <vector>
+               std::pair<int,int> foo;
+            ''',
+            'Add #include <utility> for pair<>'
+            '  [build/include_what_you_use] [4]')
+        self.assert_include_what_you_use(
+            '''#include "base/foobar.h"
+               std::vector<int> foo;
+            ''',
+            'Add #include <vector> for vector<>'
+            '  [build/include_what_you_use] [4]')
+        self.assert_include_what_you_use(
+            '''#include <vector>
+               std::set<int> foo;
+            ''',
+            'Add #include <set> for set<>'
+            '  [build/include_what_you_use] [4]')
+        self.assert_include_what_you_use(
+            '''#include "base/foobar.h"
+              hash_map<int, int> foobar;
+            ''',
+            'Add #include <hash_map> for hash_map<>'
+            '  [build/include_what_you_use] [4]')
+        self.assert_include_what_you_use(
+            '''#include "base/foobar.h"
+               bool foobar = std::less<int>(0,1);
+            ''',
+            'Add #include <functional> for less<>'
+            '  [build/include_what_you_use] [4]')
+        self.assert_include_what_you_use(
+            '''#include "base/foobar.h"
+               bool foobar = min<int>(0,1);
+            ''',
+            'Add #include <algorithm> for min  [build/include_what_you_use] [4]')
+        self.assert_include_what_you_use(
+            'void a(const string &foobar);',
+            'Add #include <string> for string  [build/include_what_you_use] [4]')
+        self.assert_include_what_you_use(
+            '''#include "base/foobar.h"
+               bool foobar = swap(0,1);
+            ''',
+            'Add #include <algorithm> for swap  [build/include_what_you_use] [4]')
+        self.assert_include_what_you_use(
+            '''#include "base/foobar.h"
+               bool foobar = transform(a.begin(), a.end(), b.start(), Foo);
+            ''',
+            'Add #include <algorithm> for transform  '
+            '[build/include_what_you_use] [4]')
+        self.assert_include_what_you_use(
+            '''#include "base/foobar.h"
+               bool foobar = min_element(a.begin(), a.end());
+            ''',
+            'Add #include <algorithm> for min_element  '
+            '[build/include_what_you_use] [4]')
+        self.assert_include_what_you_use(
+            '''foo->swap(0,1);
+               foo.swap(0,1);
+            ''',
+            '')
+        self.assert_include_what_you_use(
+            '''#include <string>
+               void a(const std::multimap<int,string> &foobar);
+            ''',
+            'Add #include <map> for multimap<>'
+            '  [build/include_what_you_use] [4]')
+        self.assert_include_what_you_use(
+            '''#include <queue>
+               void a(const std::priority_queue<int> &foobar);
+            ''',
+            '')
+        self.assert_include_what_you_use(
+             '''#include "base/basictypes.h"
+                #include "base/port.h"
+                #include <assert.h>
+                #include <string>
+                #include <vector>
+                vector<string> hajoa;''', '')
+        self.assert_include_what_you_use(
+            '''#include <string>
+               int i = numeric_limits<int>::max()
+            ''',
+            'Add #include <limits> for numeric_limits<>'
+            '  [build/include_what_you_use] [4]')
+        self.assert_include_what_you_use(
+            '''#include <limits>
+               int i = numeric_limits<int>::max()
+            ''',
+            '')
+
+        # Test the UpdateIncludeState code path.
+        mock_header_contents = ['#include "blah/foo.h"', '#include "blah/bar.h"']
+        message = self.perform_include_what_you_use(
+            '#include "config.h"\n'
+            '#include "blah/a.h"\n',
+            filename='blah/a.cpp',
+            io=MockIo(mock_header_contents))
+        self.assertEquals(message, '')
+
+        mock_header_contents = ['#include <set>']
+        message = self.perform_include_what_you_use(
+            '''#include "config.h"
+               #include "blah/a.h"
+
+               std::set<int> foo;''',
+            filename='blah/a.cpp',
+            io=MockIo(mock_header_contents))
+        self.assertEquals(message, '')
+
+        # If there's just a .cpp and the header can't be found then it's ok.
+        message = self.perform_include_what_you_use(
+            '''#include "config.h"
+               #include "blah/a.h"
+
+               std::set<int> foo;''',
+            filename='blah/a.cpp')
+        self.assertEquals(message, '')
+
+        # Make sure we find the headers with relative paths.
+        mock_header_contents = ['']
+        message = self.perform_include_what_you_use(
+            '''#include "config.h"
+               #include "%s%sa.h"
+
+               std::set<int> foo;''' % (os.path.basename(os.getcwd()), os.path.sep),
+            filename='a.cpp',
+            io=MockIo(mock_header_contents))
+        self.assertEquals(message, 'Add #include <set> for set<>  '
+                                   '[build/include_what_you_use] [4]')
+
+    def test_files_belong_to_same_module(self):
+        f = cpp_style.files_belong_to_same_module
+        self.assertEquals((True, ''), f('a.cpp', 'a.h'))
+        self.assertEquals((True, ''), f('base/google.cpp', 'base/google.h'))
+        self.assertEquals((True, ''), f('base/google_test.cpp', 'base/google.h'))
+        self.assertEquals((True, ''),
+                          f('base/google_unittest.cpp', 'base/google.h'))
+        self.assertEquals((True, ''),
+                          f('base/internal/google_unittest.cpp',
+                            'base/public/google.h'))
+        self.assertEquals((True, 'xxx/yyy/'),
+                          f('xxx/yyy/base/internal/google_unittest.cpp',
+                            'base/public/google.h'))
+        self.assertEquals((True, 'xxx/yyy/'),
+                          f('xxx/yyy/base/google_unittest.cpp',
+                            'base/public/google.h'))
+        self.assertEquals((True, ''),
+                          f('base/google_unittest.cpp', 'base/google-inl.h'))
+        self.assertEquals((True, '/home/build/google3/'),
+                          f('/home/build/google3/base/google.cpp', 'base/google.h'))
+
+        self.assertEquals((False, ''),
+                          f('/home/build/google3/base/google.cpp', 'basu/google.h'))
+        self.assertEquals((False, ''), f('a.cpp', 'b.h'))
+
+    def test_cleanse_line(self):
+        self.assertEquals('int foo = 0;  ',
+                          cpp_style.cleanse_comments('int foo = 0;  // danger!'))
+        self.assertEquals('int o = 0;',
+                          cpp_style.cleanse_comments('int /* foo */ o = 0;'))
+        self.assertEquals('foo(int a, int b);',
+                          cpp_style.cleanse_comments('foo(int a /* abc */, int b);'))
+        self.assertEqual('f(a, b);',
+                         cpp_style.cleanse_comments('f(a, /* name */ b);'))
+        self.assertEqual('f(a, b);',
+                         cpp_style.cleanse_comments('f(a /* name */, b);'))
+        self.assertEqual('f(a, b);',
+                         cpp_style.cleanse_comments('f(a, /* name */b);'))
+
+    def test_multi_line_comments(self):
+        # missing explicit is bad
+        self.assert_multi_line_lint(
+            r'''int a = 0;
+                /* multi-liner
+                class Foo {
+                Foo(int f);  // should cause a lint warning in code
+                }
+            */ ''',
+        '')
+        self.assert_multi_line_lint(
+            '''\
+            /* int a = 0; multi-liner
+            static const int b = 0;''',
+            ['Could not find end of multi-line comment'
+             '  [readability/multiline_comment] [5]',
+             'Complex multi-line /*...*/-style comment found. '
+             'Lint may give bogus warnings.  Consider replacing these with '
+             '//-style comments, with #if 0...#endif, or with more clearly '
+             'structured multi-line comments.  [readability/multiline_comment] [5]'])
+        self.assert_multi_line_lint(r'''    /* multi-line comment''',
+                                    ['Could not find end of multi-line comment'
+                                     '  [readability/multiline_comment] [5]',
+                                     'Complex multi-line /*...*/-style comment found. '
+                                     'Lint may give bogus warnings.  Consider replacing these with '
+                                     '//-style comments, with #if 0...#endif, or with more clearly '
+                                     'structured multi-line comments.  [readability/multiline_comment] [5]'])
+        self.assert_multi_line_lint(r'''    // /* comment, but not multi-line''', '')
+
+    def test_multiline_strings(self):
+        multiline_string_error_message = (
+            'Multi-line string ("...") found.  This lint script doesn\'t '
+            'do well with such strings, and may give bogus warnings.  They\'re '
+            'ugly and unnecessary, and you should use concatenation instead".'
+            '  [readability/multiline_string] [5]')
+
+        file_path = 'mydir/foo.cpp'
+
+        error_collector = ErrorCollector(self.assert_)
+        self.process_file_data(file_path, 'cpp',
+                               ['const char* str = "This is a\\',
+                                ' multiline string.";'],
+                               error_collector)
+        self.assertEquals(
+            2,  # One per line.
+            error_collector.result_list().count(multiline_string_error_message))
+
+    # Test non-explicit single-argument constructors
+    def test_explicit_single_argument_constructors(self):
+        # missing explicit is bad
+        self.assert_multi_line_lint(
+            '''\
+            class Foo {
+                Foo(int f);
+            };''',
+            'Single-argument constructors should be marked explicit.'
+            '  [runtime/explicit] [5]')
+        # missing explicit is bad, even with whitespace
+        self.assert_multi_line_lint(
+            '''\
+            class Foo {
+                Foo (int f);
+            };''',
+            ['Extra space before ( in function call  [whitespace/parens] [4]',
+             'Single-argument constructors should be marked explicit.'
+             '  [runtime/explicit] [5]'])
+        # missing explicit, with distracting comment, is still bad
+        self.assert_multi_line_lint(
+            '''\
+            class Foo {
+                Foo(int f); // simpler than Foo(blargh, blarg)
+            };''',
+            'Single-argument constructors should be marked explicit.'
+            '  [runtime/explicit] [5]')
+        # missing explicit, with qualified classname
+        self.assert_multi_line_lint(
+            '''\
+            class Qualifier::AnotherOne::Foo {
+                Foo(int f);
+            };''',
+            'Single-argument constructors should be marked explicit.'
+            '  [runtime/explicit] [5]')
+        # structs are caught as well.
+        self.assert_multi_line_lint(
+            '''\
+            struct Foo {
+                Foo(int f);
+            };''',
+            'Single-argument constructors should be marked explicit.'
+            '  [runtime/explicit] [5]')
+        # Templatized classes are caught as well.
+        self.assert_multi_line_lint(
+            '''\
+            template<typename T> class Foo {
+                Foo(int f);
+            };''',
+            'Single-argument constructors should be marked explicit.'
+            '  [runtime/explicit] [5]')
+        # proper style is okay
+        self.assert_multi_line_lint(
+            '''\
+            class Foo {
+                explicit Foo(int f);
+            };''',
+            '')
+        # two argument constructor is okay
+        self.assert_multi_line_lint(
+            '''\
+            class Foo {
+                Foo(int f, int b);
+            };''',
+            '')
+        # two argument constructor, across two lines, is okay
+        self.assert_multi_line_lint(
+            '''\
+            class Foo {
+                Foo(int f,
+                    int b);
+            };''',
+            '')
+        # non-constructor (but similar name), is okay
+        self.assert_multi_line_lint(
+            '''\
+            class Foo {
+                aFoo(int f);
+            };''',
+            '')
+        # constructor with void argument is okay
+        self.assert_multi_line_lint(
+            '''\
+            class Foo {
+                Foo(void);
+            };''',
+            '')
+        # single argument method is okay
+        self.assert_multi_line_lint(
+            '''\
+            class Foo {
+                Bar(int b);
+            };''',
+            '')
+        # comments should be ignored
+        self.assert_multi_line_lint(
+            '''\
+            class Foo {
+            // Foo(int f);
+            };''',
+            '')
+        # single argument function following class definition is okay
+        # (okay, it's not actually valid, but we don't want a false positive)
+        self.assert_multi_line_lint(
+            '''\
+            class Foo {
+                Foo(int f, int b);
+            };
+            Foo(int f);''',
+            '')
+        # single argument function is okay
+        self.assert_multi_line_lint(
+            '''static Foo(int f);''',
+            '')
+        # single argument copy constructor is okay.
+        self.assert_multi_line_lint(
+            '''\
+            class Foo {
+                Foo(const Foo&);
+            };''',
+            '')
+        self.assert_multi_line_lint(
+            '''\
+            class Foo {
+                Foo(Foo&);
+            };''',
+            '')
+
+    def test_slash_star_comment_on_single_line(self):
+        self.assert_multi_line_lint(
+            '''/* static */ Foo(int f);''',
+            '')
+        self.assert_multi_line_lint(
+            '''/*/ static */  Foo(int f);''',
+            '')
+        self.assert_multi_line_lint(
+            '''/*/ static Foo(int f);''',
+            'Could not find end of multi-line comment'
+            '  [readability/multiline_comment] [5]')
+        self.assert_multi_line_lint(
+            '''    /*/ static Foo(int f);''',
+            'Could not find end of multi-line comment'
+            '  [readability/multiline_comment] [5]')
+
+    # Test suspicious usage of "if" like this:
+    # if (a == b) {
+    #   DoSomething();
+    # } if (a == c) {   // Should be "else if".
+    #   DoSomething();  // This gets called twice if a == b && a == c.
+    # }
+    def test_suspicious_usage_of_if(self):
+        self.assert_lint(
+            '    if (a == b) {',
+            '')
+        self.assert_lint(
+            '    } if (a == b) {',
+            'Did you mean "else if"? If not, start a new line for "if".'
+            '  [readability/braces] [4]')
+
+    # Test suspicious usage of memset. Specifically, a 0
+    # as the final argument is almost certainly an error.
+    def test_suspicious_usage_of_memset(self):
+        # Normal use is okay.
+        self.assert_lint(
+            '    memset(buf, 0, sizeof(buf))',
+            '')
+
+        # A 0 as the final argument is almost certainly an error.
+        self.assert_lint(
+            '    memset(buf, sizeof(buf), 0)',
+            'Did you mean "memset(buf, 0, sizeof(buf))"?'
+            '  [runtime/memset] [4]')
+        self.assert_lint(
+            '    memset(buf, xsize * ysize, 0)',
+            'Did you mean "memset(buf, 0, xsize * ysize)"?'
+            '  [runtime/memset] [4]')
+
+        # There is legitimate test code that uses this form.
+        # This is okay since the second argument is a literal.
+        self.assert_lint(
+            "    memset(buf, 'y', 0)",
+            '')
+        self.assert_lint(
+            '    memset(buf, 4, 0)',
+            '')
+        self.assert_lint(
+            '    memset(buf, -1, 0)',
+            '')
+        self.assert_lint(
+            '    memset(buf, 0xF1, 0)',
+            '')
+        self.assert_lint(
+            '    memset(buf, 0xcd, 0)',
+            '')
+
+    def test_check_posix_threading(self):
+        self.assert_lint('sctime_r()', '')
+        self.assert_lint('strtok_r()', '')
+        self.assert_lint('    strtok_r(foo, ba, r)', '')
+        self.assert_lint('brand()', '')
+        self.assert_lint('_rand()', '')
+        self.assert_lint('.rand()', '')
+        self.assert_lint('>rand()', '')
+        self.assert_lint('rand()',
+                         'Consider using rand_r(...) instead of rand(...)'
+                         ' for improved thread safety.'
+                         '  [runtime/threadsafe_fn] [2]')
+        self.assert_lint('strtok()',
+                         'Consider using strtok_r(...) '
+                         'instead of strtok(...)'
+                         ' for improved thread safety.'
+                         '  [runtime/threadsafe_fn] [2]')
+
+    # Test potential format string bugs like printf(foo).
+    def test_format_strings(self):
+        self.assert_lint('printf("foo")', '')
+        self.assert_lint('printf("foo: %s", foo)', '')
+        self.assert_lint('DocidForPrintf(docid)', '')  # Should not trigger.
+        self.assert_lint(
+            'printf(foo)',
+            'Potential format string bug. Do printf("%s", foo) instead.'
+            '  [runtime/printf] [4]')
+        self.assert_lint(
+            'printf(foo.c_str())',
+            'Potential format string bug. '
+            'Do printf("%s", foo.c_str()) instead.'
+            '  [runtime/printf] [4]')
+        self.assert_lint(
+            'printf(foo->c_str())',
+            'Potential format string bug. '
+            'Do printf("%s", foo->c_str()) instead.'
+            '  [runtime/printf] [4]')
+        self.assert_lint(
+            'StringPrintf(foo)',
+            'Potential format string bug. Do StringPrintf("%s", foo) instead.'
+            ''
+            '  [runtime/printf] [4]')
+
+    # Variable-length arrays are not permitted.
+    def test_variable_length_array_detection(self):
+        errmsg = ('Do not use variable-length arrays.  Use an appropriately named '
+                  "('k' followed by CamelCase) compile-time constant for the size."
+                  '  [runtime/arrays] [1]')
+
+        self.assert_lint('int a[any_old_variable];', errmsg)
+        self.assert_lint('int doublesize[some_var * 2];', errmsg)
+        self.assert_lint('int a[afunction()];', errmsg)
+        self.assert_lint('int a[function(kMaxFooBars)];', errmsg)
+        self.assert_lint('bool aList[items_->size()];', errmsg)
+        self.assert_lint('namespace::Type buffer[len+1];', errmsg)
+
+        self.assert_lint('int a[64];', '')
+        self.assert_lint('int a[0xFF];', '')
+        self.assert_lint('int first[256], second[256];', '')
+        self.assert_lint('int arrayName[kCompileTimeConstant];', '')
+        self.assert_lint('char buf[somenamespace::kBufSize];', '')
+        self.assert_lint('int arrayName[ALL_CAPS];', '')
+        self.assert_lint('AClass array1[foo::bar::ALL_CAPS];', '')
+        self.assert_lint('int a[kMaxStrLen + 1];', '')
+        self.assert_lint('int a[sizeof(foo)];', '')
+        self.assert_lint('int a[sizeof(*foo)];', '')
+        self.assert_lint('int a[sizeof foo];', '')
+        self.assert_lint('int a[sizeof(struct Foo)];', '')
+        self.assert_lint('int a[128 - sizeof(const bar)];', '')
+        self.assert_lint('int a[(sizeof(foo) * 4)];', '')
+        self.assert_lint('int a[(arraysize(fixed_size_array)/2) << 1];', 'Missing spaces around /  [whitespace/operators] [3]')
+        self.assert_lint('delete a[some_var];', '')
+        self.assert_lint('return a[some_var];', '')
+
+    # Brace usage
+    def test_braces(self):
+        # Braces shouldn't be followed by a ; unless they're defining a struct
+        # or initializing an array
+        self.assert_lint('int a[3] = { 1, 2, 3 };', '')
+        self.assert_lint(
+            '''\
+            const int foo[] =
+                {1, 2, 3 };''',
+            '')
+        # For single line, unmatched '}' with a ';' is ignored (not enough context)
+        self.assert_multi_line_lint(
+            '''\
+            int a[3] = { 1,
+                2,
+                3 };''',
+            '')
+        self.assert_multi_line_lint(
+            '''\
+            int a[2][3] = { { 1, 2 },
+                { 3, 4 } };''',
+            '')
+        self.assert_multi_line_lint(
+            '''\
+            int a[2][3] =
+                { { 1, 2 },
+                { 3, 4 } };''',
+            '')
+
+    # CHECK/EXPECT_TRUE/EXPECT_FALSE replacements
+    def test_check_check(self):
+        self.assert_lint('CHECK(x == 42)',
+                         'Consider using CHECK_EQ instead of CHECK(a == b)'
+                         '  [readability/check] [2]')
+        self.assert_lint('CHECK(x != 42)',
+                         'Consider using CHECK_NE instead of CHECK(a != b)'
+                         '  [readability/check] [2]')
+        self.assert_lint('CHECK(x >= 42)',
+                         'Consider using CHECK_GE instead of CHECK(a >= b)'
+                         '  [readability/check] [2]')
+        self.assert_lint('CHECK(x > 42)',
+                         'Consider using CHECK_GT instead of CHECK(a > b)'
+                         '  [readability/check] [2]')
+        self.assert_lint('CHECK(x <= 42)',
+                         'Consider using CHECK_LE instead of CHECK(a <= b)'
+                         '  [readability/check] [2]')
+        self.assert_lint('CHECK(x < 42)',
+                         'Consider using CHECK_LT instead of CHECK(a < b)'
+                         '  [readability/check] [2]')
+
+        self.assert_lint('DCHECK(x == 42)',
+                         'Consider using DCHECK_EQ instead of DCHECK(a == b)'
+                         '  [readability/check] [2]')
+        self.assert_lint('DCHECK(x != 42)',
+                         'Consider using DCHECK_NE instead of DCHECK(a != b)'
+                         '  [readability/check] [2]')
+        self.assert_lint('DCHECK(x >= 42)',
+                         'Consider using DCHECK_GE instead of DCHECK(a >= b)'
+                         '  [readability/check] [2]')
+        self.assert_lint('DCHECK(x > 42)',
+                         'Consider using DCHECK_GT instead of DCHECK(a > b)'
+                         '  [readability/check] [2]')
+        self.assert_lint('DCHECK(x <= 42)',
+                         'Consider using DCHECK_LE instead of DCHECK(a <= b)'
+                         '  [readability/check] [2]')
+        self.assert_lint('DCHECK(x < 42)',
+                         'Consider using DCHECK_LT instead of DCHECK(a < b)'
+                         '  [readability/check] [2]')
+
+        self.assert_lint(
+            'EXPECT_TRUE("42" == x)',
+            'Consider using EXPECT_EQ instead of EXPECT_TRUE(a == b)'
+            '  [readability/check] [2]')
+        self.assert_lint(
+            'EXPECT_TRUE("42" != x)',
+            'Consider using EXPECT_NE instead of EXPECT_TRUE(a != b)'
+            '  [readability/check] [2]')
+        self.assert_lint(
+            'EXPECT_TRUE(+42 >= x)',
+            'Consider using EXPECT_GE instead of EXPECT_TRUE(a >= b)'
+            '  [readability/check] [2]')
+        self.assert_lint(
+            'EXPECT_TRUE_M(-42 > x)',
+            'Consider using EXPECT_GT_M instead of EXPECT_TRUE_M(a > b)'
+            '  [readability/check] [2]')
+        self.assert_lint(
+            'EXPECT_TRUE_M(42U <= x)',
+            'Consider using EXPECT_LE_M instead of EXPECT_TRUE_M(a <= b)'
+            '  [readability/check] [2]')
+        self.assert_lint(
+            'EXPECT_TRUE_M(42L < x)',
+            'Consider using EXPECT_LT_M instead of EXPECT_TRUE_M(a < b)'
+            '  [readability/check] [2]')
+
+        self.assert_lint(
+            'EXPECT_FALSE(x == 42)',
+            'Consider using EXPECT_NE instead of EXPECT_FALSE(a == b)'
+            '  [readability/check] [2]')
+        self.assert_lint(
+            'EXPECT_FALSE(x != 42)',
+            'Consider using EXPECT_EQ instead of EXPECT_FALSE(a != b)'
+            '  [readability/check] [2]')
+        self.assert_lint(
+            'EXPECT_FALSE(x >= 42)',
+            'Consider using EXPECT_LT instead of EXPECT_FALSE(a >= b)'
+            '  [readability/check] [2]')
+        self.assert_lint(
+            'ASSERT_FALSE(x > 42)',
+            'Consider using ASSERT_LE instead of ASSERT_FALSE(a > b)'
+            '  [readability/check] [2]')
+        self.assert_lint(
+            'ASSERT_FALSE(x <= 42)',
+            'Consider using ASSERT_GT instead of ASSERT_FALSE(a <= b)'
+            '  [readability/check] [2]')
+        self.assert_lint(
+            'ASSERT_FALSE_M(x < 42)',
+            'Consider using ASSERT_GE_M instead of ASSERT_FALSE_M(a < b)'
+            '  [readability/check] [2]')
+
+        self.assert_lint('CHECK(some_iterator == obj.end())', '')
+        self.assert_lint('EXPECT_TRUE(some_iterator == obj.end())', '')
+        self.assert_lint('EXPECT_FALSE(some_iterator == obj.end())', '')
+
+        self.assert_lint('CHECK(CreateTestFile(dir, (1 << 20)));', '')
+        self.assert_lint('CHECK(CreateTestFile(dir, (1 >> 20)));', '')
+
+        self.assert_lint('CHECK(x<42)',
+                         ['Missing spaces around <'
+                          '  [whitespace/operators] [3]',
+                          'Consider using CHECK_LT instead of CHECK(a < b)'
+                          '  [readability/check] [2]'])
+        self.assert_lint('CHECK(x>42)',
+                         'Consider using CHECK_GT instead of CHECK(a > b)'
+                         '  [readability/check] [2]')
+
+        self.assert_lint(
+            '    EXPECT_TRUE(42 < x) // Random comment.',
+            'Consider using EXPECT_LT instead of EXPECT_TRUE(a < b)'
+            '  [readability/check] [2]')
+        self.assert_lint(
+            'EXPECT_TRUE( 42 < x )',
+            ['Extra space after ( in function call'
+             '  [whitespace/parens] [4]',
+             'Consider using EXPECT_LT instead of EXPECT_TRUE(a < b)'
+             '  [readability/check] [2]'])
+        self.assert_lint(
+            'CHECK("foo" == "foo")',
+            'Consider using CHECK_EQ instead of CHECK(a == b)'
+            '  [readability/check] [2]')
+
+        self.assert_lint('CHECK_EQ("foo", "foo")', '')
+
+    def test_brace_at_begin_of_line(self):
+        self.assert_lint('{',
+                         'This { should be at the end of the previous line'
+                         '  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            '#endif\n'
+            '{\n'
+            '}\n',
+            '')
+        self.assert_multi_line_lint(
+            'if (condition) {',
+            '')
+        self.assert_multi_line_lint(
+            '    MACRO1(macroArg) {',
+            '')
+        self.assert_multi_line_lint(
+            'ACCESSOR_GETTER(MessageEventPorts) {',
+            'Place brace on its own line for function definitions.  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            'int foo() {',
+            'Place brace on its own line for function definitions.  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            'int foo() const {',
+            'Place brace on its own line for function definitions.  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            'int foo() const OVERRIDE {',
+            'Place brace on its own line for function definitions.  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            'int foo() OVERRIDE {',
+            'Place brace on its own line for function definitions.  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            'int foo() const\n'
+            '{\n'
+            '}\n',
+            '')
+        self.assert_multi_line_lint(
+            'int foo() OVERRIDE\n'
+            '{\n'
+            '}\n',
+            '')
+        self.assert_multi_line_lint(
+            'if (condition\n'
+            '    && condition2\n'
+            '    && condition3) {\n'
+            '}\n',
+            '')
+
+    def test_mismatching_spaces_in_parens(self):
+        self.assert_lint('if (foo ) {', 'Extra space before ) in if'
+                         '  [whitespace/parens] [5]')
+        self.assert_lint('switch ( foo) {', 'Extra space after ( in switch'
+                         '  [whitespace/parens] [5]')
+        self.assert_lint('for (foo; ba; bar ) {', 'Extra space before ) in for'
+                         '  [whitespace/parens] [5]')
+        self.assert_lint('for ((foo); (ba); (bar) ) {', 'Extra space before ) in for'
+                         '  [whitespace/parens] [5]')
+        self.assert_lint('for (; foo; bar) {', '')
+        self.assert_lint('for (; (foo); (bar)) {', '')
+        self.assert_lint('for ( ; foo; bar) {', '')
+        self.assert_lint('for ( ; (foo); (bar)) {', '')
+        self.assert_lint('for ( ; foo; bar ) {', 'Extra space before ) in for'
+                         '  [whitespace/parens] [5]')
+        self.assert_lint('for ( ; (foo); (bar) ) {', 'Extra space before ) in for'
+                         '  [whitespace/parens] [5]')
+        self.assert_lint('for (foo; bar; ) {', '')
+        self.assert_lint('for ((foo); (bar); ) {', '')
+        self.assert_lint('foreach (foo, foos ) {', 'Extra space before ) in foreach'
+                         '  [whitespace/parens] [5]')
+        self.assert_lint('foreach ( foo, foos) {', 'Extra space after ( in foreach'
+                         '  [whitespace/parens] [5]')
+        self.assert_lint('while (  foo) {', 'Extra space after ( in while'
+                         '  [whitespace/parens] [5]')
+
+    def test_spacing_for_fncall(self):
+        self.assert_lint('if (foo) {', '')
+        self.assert_lint('for (foo;bar;baz) {', '')
+        self.assert_lint('foreach (foo, foos) {', '')
+        self.assert_lint('while (foo) {', '')
+        self.assert_lint('switch (foo) {', '')
+        self.assert_lint('new (RenderArena()) RenderInline(document())', '')
+        self.assert_lint('foo( bar)', 'Extra space after ( in function call'
+                         '  [whitespace/parens] [4]')
+        self.assert_lint('foobar( \\', '')
+        self.assert_lint('foobar(     \\', '')
+        self.assert_lint('( a + b)', 'Extra space after ('
+                         '  [whitespace/parens] [2]')
+        self.assert_lint('((a+b))', '')
+        self.assert_lint('foo (foo)', 'Extra space before ( in function call'
+                         '  [whitespace/parens] [4]')
+        self.assert_lint('#elif (foo(bar))', '')
+        self.assert_lint('#elif (foo(bar) && foo(baz))', '')
+        self.assert_lint('typedef foo (*foo)(foo)', '')
+        self.assert_lint('typedef foo (*foo12bar_)(foo)', '')
+        self.assert_lint('typedef foo (Foo::*bar)(foo)', '')
+        self.assert_lint('foo (Foo::*bar)(',
+                         'Extra space before ( in function call'
+                         '  [whitespace/parens] [4]')
+        self.assert_lint('typedef foo (Foo::*bar)(', '')
+        self.assert_lint('(foo)(bar)', '')
+        self.assert_lint('Foo (*foo)(bar)', '')
+        self.assert_lint('Foo (*foo)(Bar bar,', '')
+        self.assert_lint('char (*p)[sizeof(foo)] = &foo', '')
+        self.assert_lint('char (&ref)[sizeof(foo)] = &foo', '')
+        self.assert_lint('const char32 (*table[])[6];', '')
+
+    def test_spacing_before_braces(self):
+        self.assert_lint('if (foo){', 'Missing space before {'
+                         '  [whitespace/braces] [5]')
+        self.assert_lint('for{', 'Missing space before {'
+                         '  [whitespace/braces] [5]')
+        self.assert_lint('for {', '')
+        self.assert_lint('EXPECT_DEBUG_DEATH({', '')
+
+    def test_spacing_between_braces(self):
+        self.assert_lint('    { }', '')
+        self.assert_lint('    {}', 'Missing space inside { }.  [whitespace/braces] [5]')
+        self.assert_lint('    {   }', 'Too many spaces inside { }.  [whitespace/braces] [5]')
+
+    def test_spacing_around_else(self):
+        self.assert_lint('}else {', 'Missing space before else'
+                         '  [whitespace/braces] [5]')
+        self.assert_lint('} else{', 'Missing space before {'
+                         '  [whitespace/braces] [5]')
+        self.assert_lint('} else {', '')
+        self.assert_lint('} else if', '')
+
+    def test_spacing_for_binary_ops(self):
+        self.assert_lint('if (foo<=bar) {', 'Missing spaces around <='
+                         '  [whitespace/operators] [3]')
+        self.assert_lint('if (foo<bar) {', 'Missing spaces around <'
+                         '  [whitespace/operators] [3]')
+        self.assert_lint('if (foo<bar->baz) {', 'Missing spaces around <'
+                         '  [whitespace/operators] [3]')
+        self.assert_lint('if (foo<bar->bar) {', 'Missing spaces around <'
+                         '  [whitespace/operators] [3]')
+        self.assert_lint('typedef hash_map<Foo, Bar', 'Missing spaces around <'
+                         '  [whitespace/operators] [3]')
+        self.assert_lint('typedef hash_map<FoooooType, BaaaaarType,', '')
+        self.assert_lint('a<Foo> t+=b;', 'Missing spaces around +='
+                         '  [whitespace/operators] [3]')
+        self.assert_lint('a<Foo> t-=b;', 'Missing spaces around -='
+                         '  [whitespace/operators] [3]')
+        self.assert_lint('a<Foo*> t*=b;', 'Missing spaces around *='
+                         '  [whitespace/operators] [3]')
+        self.assert_lint('a<Foo*> t/=b;', 'Missing spaces around /='
+                         '  [whitespace/operators] [3]')
+        self.assert_lint('a<Foo*> t|=b;', 'Missing spaces around |='
+                         '  [whitespace/operators] [3]')
+        self.assert_lint('a<Foo*> t&=b;', 'Missing spaces around &='
+                         '  [whitespace/operators] [3]')
+        self.assert_lint('a<Foo*> t<<=b;', 'Missing spaces around <<='
+                         '  [whitespace/operators] [3]')
+        self.assert_lint('a<Foo*> t>>=b;', 'Missing spaces around >>='
+                         '  [whitespace/operators] [3]')
+        self.assert_lint('a<Foo*> t>>=&b|c;', 'Missing spaces around >>='
+                         '  [whitespace/operators] [3]')
+        self.assert_lint('a<Foo*> t<<=*b/c;', 'Missing spaces around <<='
+                         '  [whitespace/operators] [3]')
+        self.assert_lint('a<Foo> t -= b;', '')
+        self.assert_lint('a<Foo> t += b;', '')
+        self.assert_lint('a<Foo*> t *= b;', '')
+        self.assert_lint('a<Foo*> t /= b;', '')
+        self.assert_lint('a<Foo*> t |= b;', '')
+        self.assert_lint('a<Foo*> t &= b;', '')
+        self.assert_lint('a<Foo*> t <<= b;', '')
+        self.assert_lint('a<Foo*> t >>= b;', '')
+        self.assert_lint('a<Foo*> t >>= &b|c;', 'Missing spaces around |'
+                         '  [whitespace/operators] [3]')
+        self.assert_lint('a<Foo*> t <<= *b/c;', 'Missing spaces around /'
+                         '  [whitespace/operators] [3]')
+        self.assert_lint('a<Foo*> t <<= b/c; //Test', [
+                         'Should have a space between // and comment  '
+                         '[whitespace/comments] [4]', 'Missing'
+                         ' spaces around /  [whitespace/operators] [3]'])
+        self.assert_lint('a<Foo*> t <<= b||c;  //Test', ['One space before end'
+                         ' of line comments  [whitespace/comments] [5]',
+                         'Should have a space between // and comment  '
+                         '[whitespace/comments] [4]',
+                         'Missing spaces around ||  [whitespace/operators] [3]'])
+        self.assert_lint('a<Foo*> t <<= b&&c; // Test', 'Missing spaces around'
+                         ' &&  [whitespace/operators] [3]')
+        self.assert_lint('a<Foo*> t <<= b&&&c; // Test', 'Missing spaces around'
+                         ' &&  [whitespace/operators] [3]')
+        self.assert_lint('a<Foo*> t <<= b&&*c; // Test', 'Missing spaces around'
+                         ' &&  [whitespace/operators] [3]')
+        self.assert_lint('a<Foo*> t <<= b && *c; // Test', '')
+        self.assert_lint('a<Foo*> t <<= b && &c; // Test', '')
+        self.assert_lint('a<Foo*> t <<= b || &c;  /*Test', 'Complex multi-line '
+                         '/*...*/-style comment found. Lint may give bogus '
+                         'warnings.  Consider replacing these with //-style'
+                         ' comments, with #if 0...#endif, or with more clearly'
+                         ' structured multi-line comments.  [readability/multiline_comment] [5]')
+        self.assert_lint('a<Foo&> t <<= &b | &c;', '')
+        self.assert_lint('a<Foo*> t <<= &b & &c; // Test', '')
+        self.assert_lint('a<Foo*> t <<= *b / &c; // Test', '')
+        self.assert_lint('if (a=b == 1)', 'Missing spaces around =  [whitespace/operators] [4]')
+        self.assert_lint('a = 1<<20', 'Missing spaces around <<  [whitespace/operators] [3]')
+        self.assert_lint('if (a = b == 1)', '')
+        self.assert_lint('a = 1 << 20', '')
+        self.assert_multi_line_lint('#include <sys/io.h>\n', '')
+        self.assert_multi_line_lint('#import <foo/bar.h>\n', '')
+
+    def test_operator_methods(self):
+        self.assert_lint('String operator+(const String&, const String&);', '')
+        self.assert_lint('String operator/(const String&, const String&);', '')
+        self.assert_lint('bool operator==(const String&, const String&);', '')
+        self.assert_lint('String& operator-=(const String&, const String&);', '')
+        self.assert_lint('String& operator+=(const String&, const String&);', '')
+        self.assert_lint('String& operator*=(const String&, const String&);', '')
+        self.assert_lint('String& operator%=(const String&, const String&);', '')
+        self.assert_lint('String& operator&=(const String&, const String&);', '')
+        self.assert_lint('String& operator<<=(const String&, const String&);', '')
+        self.assert_lint('String& operator>>=(const String&, const String&);', '')
+        self.assert_lint('String& operator|=(const String&, const String&);', '')
+        self.assert_lint('String& operator^=(const String&, const String&);', '')
+
+    def test_spacing_before_last_semicolon(self):
+        self.assert_lint('call_function() ;',
+                         'Extra space before last semicolon. If this should be an '
+                         'empty statement, use { } instead.'
+                         '  [whitespace/semicolon] [5]')
+        self.assert_lint('while (true) ;',
+                         'Extra space before last semicolon. If this should be an '
+                         'empty statement, use { } instead.'
+                         '  [whitespace/semicolon] [5]')
+        self.assert_lint('default:;',
+                         'Semicolon defining empty statement. Use { } instead.'
+                         '  [whitespace/semicolon] [5]')
+        self.assert_lint('        ;',
+                         'Line contains only semicolon. If this should be an empty '
+                         'statement, use { } instead.'
+                         '  [whitespace/semicolon] [5]')
+        self.assert_lint('for (int i = 0; ;', '')
+
+    # Static or global STL strings.
+    def test_static_or_global_stlstrings(self):
+        self.assert_lint('string foo;',
+                         'For a static/global string constant, use a C style '
+                         'string instead: "char foo[]".'
+                         '  [runtime/string] [4]')
+        self.assert_lint('string kFoo = "hello"; // English',
+                         'For a static/global string constant, use a C style '
+                         'string instead: "char kFoo[]".'
+                         '  [runtime/string] [4]')
+        self.assert_lint('static string foo;',
+                         'For a static/global string constant, use a C style '
+                         'string instead: "static char foo[]".'
+                         '  [runtime/string] [4]')
+        self.assert_lint('static const string foo;',
+                         'For a static/global string constant, use a C style '
+                         'string instead: "static const char foo[]".'
+                         '  [runtime/string] [4]')
+        self.assert_lint('string Foo::bar;',
+                         'For a static/global string constant, use a C style '
+                         'string instead: "char Foo::bar[]".'
+                         '  [runtime/string] [4]')
+        # Rare case.
+        self.assert_lint('string foo("foobar");',
+                         'For a static/global string constant, use a C style '
+                         'string instead: "char foo[]".'
+                         '  [runtime/string] [4]')
+        # Should not catch local or member variables.
+        self.assert_lint('    string foo', '')
+        # Should not catch functions.
+        self.assert_lint('string EmptyString() { return ""; }', '')
+        self.assert_lint('string EmptyString () { return ""; }', '')
+        self.assert_lint('string VeryLongNameFunctionSometimesEndsWith(\n'
+                         '    VeryLongNameType veryLongNameVariable) { }', '')
+        self.assert_lint('template<>\n'
+                         'string FunctionTemplateSpecialization<SomeType>(\n'
+                         '    int x) { return ""; }', '')
+        self.assert_lint('template<>\n'
+                         'string FunctionTemplateSpecialization<vector<A::B>* >(\n'
+                         '    int x) { return ""; }', '')
+
+        # should not catch methods of template classes.
+        self.assert_lint('string Class<Type>::Method() const\n'
+                         '{\n'
+                         '    return "";\n'
+                         '}\n', '')
+        self.assert_lint('string Class<Type>::Method(\n'
+                         '    int arg) const\n'
+                         '{\n'
+                         '    return "";\n'
+                         '}\n', '')
+
+    def test_no_spaces_in_function_calls(self):
+        self.assert_lint('TellStory(1, 3);',
+                         '')
+        self.assert_lint('TellStory(1, 3 );',
+                         'Extra space before )'
+                         '  [whitespace/parens] [2]')
+        self.assert_lint('TellStory(1 /* wolf */, 3 /* pigs */);',
+                         '')
+        self.assert_multi_line_lint('#endif\n    );',
+                                    '')
+
+    def test_one_spaces_between_code_and_comments(self):
+        self.assert_lint('} // namespace foo',
+                         '')
+        self.assert_lint('}// namespace foo',
+                         'One space before end of line comments'
+                         '  [whitespace/comments] [5]')
+        self.assert_lint('printf("foo"); // Outside quotes.',
+                         '')
+        self.assert_lint('int i = 0; // Having one space is fine.','')
+        self.assert_lint('int i = 0;  // Having two spaces is bad.',
+                         'One space before end of line comments'
+                         '  [whitespace/comments] [5]')
+        self.assert_lint('int i = 0;   // Having three spaces is bad.',
+                         'One space before end of line comments'
+                         '  [whitespace/comments] [5]')
+        self.assert_lint('// Top level comment', '')
+        self.assert_lint('    // Line starts with four spaces.', '')
+        self.assert_lint('foo();\n'
+                         '{ // A scope is opening.', '')
+        self.assert_lint('    foo();\n'
+                         '    { // An indented scope is opening.', '')
+        self.assert_lint('if (foo) { // not a pure scope',
+                         '')
+        self.assert_lint('printf("// In quotes.")', '')
+        self.assert_lint('printf("\\"%s // In quotes.")', '')
+        self.assert_lint('printf("%s", "// In quotes.")', '')
+
+    def test_one_spaces_after_punctuation_in_comments(self):
+        self.assert_lint('int a; // This is a sentence.',
+                         '')
+        self.assert_lint('int a; // This is a sentence.  ',
+                         'Line ends in whitespace.  Consider deleting these extra spaces.  [whitespace/end_of_line] [4]')
+        self.assert_lint('int a; // This is a sentence. This is a another sentence.',
+                         '')
+        self.assert_lint('int a; // This is a sentence.  This is a another sentence.',
+                         'Should have only a single space after a punctuation in a comment.  [whitespace/comments] [5]')
+        self.assert_lint('int a; // This is a sentence!  This is a another sentence.',
+                         'Should have only a single space after a punctuation in a comment.  [whitespace/comments] [5]')
+        self.assert_lint('int a; // Why did I write this?  This is a another sentence.',
+                         'Should have only a single space after a punctuation in a comment.  [whitespace/comments] [5]')
+        self.assert_lint('int a; // Elementary,  my dear.',
+                         'Should have only a single space after a punctuation in a comment.  [whitespace/comments] [5]')
+        self.assert_lint('int a; // The following should be clear:  Is it?',
+                         'Should have only a single space after a punctuation in a comment.  [whitespace/comments] [5]')
+        self.assert_lint('int a; // Look at the follow semicolon;  I hope this gives an error.',
+                         'Should have only a single space after a punctuation in a comment.  [whitespace/comments] [5]')
+
+    def test_space_after_comment_marker(self):
+        self.assert_lint('//', '')
+        self.assert_lint('//x', 'Should have a space between // and comment'
+                         '  [whitespace/comments] [4]')
+        self.assert_lint('// x', '')
+        self.assert_lint('//----', '')
+        self.assert_lint('//====', '')
+        self.assert_lint('//////', '')
+        self.assert_lint('////// x', '')
+        self.assert_lint('/// x', '')
+        self.assert_lint('////x', 'Should have a space between // and comment'
+                         '  [whitespace/comments] [4]')
+
+    def test_newline_at_eof(self):
+        def do_test(self, data, is_missing_eof):
+            error_collector = ErrorCollector(self.assert_)
+            self.process_file_data('foo.cpp', 'cpp', data.split('\n'),
+                                   error_collector)
+            # The warning appears only once.
+            self.assertEquals(
+                int(is_missing_eof),
+                error_collector.results().count(
+                    'Could not find a newline character at the end of the file.'
+                    '  [whitespace/ending_newline] [5]'))
+
+        do_test(self, '// Newline\n// at EOF\n', False)
+        do_test(self, '// No newline\n// at EOF', True)
+
+    def test_invalid_utf8(self):
+        def do_test(self, raw_bytes, has_invalid_utf8):
+            error_collector = ErrorCollector(self.assert_)
+            self.process_file_data('foo.cpp', 'cpp',
+                                   unicode(raw_bytes, 'utf8', 'replace').split('\n'),
+                                   error_collector)
+            # The warning appears only once.
+            self.assertEquals(
+                int(has_invalid_utf8),
+                error_collector.results().count(
+                    'Line contains invalid UTF-8'
+                    ' (or Unicode replacement character).'
+                    '  [readability/utf8] [5]'))
+
+        do_test(self, 'Hello world\n', False)
+        do_test(self, '\xe9\x8e\xbd\n', False)
+        do_test(self, '\xe9x\x8e\xbd\n', True)
+        # This is the encoding of the replacement character itself (which
+        # you can see by evaluating codecs.getencoder('utf8')(u'\ufffd')).
+        do_test(self, '\xef\xbf\xbd\n', True)
+
+    def test_is_blank_line(self):
+        self.assert_(cpp_style.is_blank_line(''))
+        self.assert_(cpp_style.is_blank_line(' '))
+        self.assert_(cpp_style.is_blank_line(' \t\r\n'))
+        self.assert_(not cpp_style.is_blank_line('int a;'))
+        self.assert_(not cpp_style.is_blank_line('{'))
+
+    def test_blank_lines_check(self):
+        self.assert_blank_lines_check(['{\n', '\n', '\n', '}\n'], 1, 1)
+        self.assert_blank_lines_check(['  if (foo) {\n', '\n', '  }\n'], 1, 1)
+        self.assert_blank_lines_check(
+            ['\n', '// {\n', '\n', '\n', '// Comment\n', '{\n', '}\n'], 0, 0)
+        self.assert_blank_lines_check(['\n', 'run("{");\n', '\n'], 0, 0)
+        self.assert_blank_lines_check(['\n', '  if (foo) { return 0; }\n', '\n'], 0, 0)
+
+    def test_allow_blank_line_before_closing_namespace(self):
+        error_collector = ErrorCollector(self.assert_)
+        self.process_file_data('foo.cpp', 'cpp',
+                               ['namespace {', '', '}  // namespace'],
+                               error_collector)
+        self.assertEquals(0, error_collector.results().count(
+            'Blank line at the end of a code block.  Is this needed?'
+            '  [whitespace/blank_line] [3]'))
+
+    def test_allow_blank_line_before_if_else_chain(self):
+        error_collector = ErrorCollector(self.assert_)
+        self.process_file_data('foo.cpp', 'cpp',
+                               ['if (hoge) {',
+                                '',  # No warning
+                                '} else if (piyo) {',
+                                '',  # No warning
+                                '} else if (piyopiyo) {',
+                                '  hoge = true;',  # No warning
+                                '} else {',
+                                '',  # Warning on this line
+                                '}'],
+                               error_collector)
+        self.assertEquals(1, error_collector.results().count(
+            'Blank line at the end of a code block.  Is this needed?'
+            '  [whitespace/blank_line] [3]'))
+
+    def test_else_on_same_line_as_closing_braces(self):
+        error_collector = ErrorCollector(self.assert_)
+        self.process_file_data('foo.cpp', 'cpp',
+                               ['if (hoge) {',
+                                '',
+                                '}',
+                                ' else {'  # Warning on this line
+                                '',
+                                '}'],
+                               error_collector)
+        self.assertEquals(1, error_collector.results().count(
+            'An else should appear on the same line as the preceding }'
+            '  [whitespace/newline] [4]'))
+
+    def test_else_clause_not_on_same_line_as_else(self):
+        self.assert_lint('    else DoSomethingElse();',
+                         'Else clause should never be on same line as else '
+                         '(use 2 lines)  [whitespace/newline] [4]')
+        self.assert_lint('    else ifDoSomethingElse();',
+                         'Else clause should never be on same line as else '
+                         '(use 2 lines)  [whitespace/newline] [4]')
+        self.assert_lint('    else if (blah) {', '')
+        self.assert_lint('    variable_ends_in_else = true;', '')
+
+    def test_comma(self):
+        self.assert_lint('a = f(1,2);',
+                         'Missing space after ,  [whitespace/comma] [3]')
+        self.assert_lint('int tmp=a,a=b,b=tmp;',
+                         ['Missing spaces around =  [whitespace/operators] [4]',
+                          'Missing space after ,  [whitespace/comma] [3]'])
+        self.assert_lint('f(a, /* name */ b);', '')
+        self.assert_lint('f(a, /* name */b);', '')
+
+    def test_declaration(self):
+        self.assert_lint('int a;', '')
+        self.assert_lint('int   a;', 'Extra space between int and a  [whitespace/declaration] [3]')
+        self.assert_lint('int*  a;', 'Extra space between int* and a  [whitespace/declaration] [3]')
+        self.assert_lint('else if { }', '')
+        self.assert_lint('else   if { }', 'Extra space between else and if  [whitespace/declaration] [3]')
+
+    def test_pointer_reference_marker_location(self):
+        self.assert_lint('int* b;', '', 'foo.cpp')
+        self.assert_lint('int *b;',
+                         'Declaration has space between type name and * in int *b  [whitespace/declaration] [3]',
+                         'foo.cpp')
+        self.assert_lint('return *b;', '', 'foo.cpp')
+        self.assert_lint('delete *b;', '', 'foo.cpp')
+        self.assert_lint('int *b;', '', 'foo.c')
+        self.assert_lint('int* b;',
+                         'Declaration has space between * and variable name in int* b  [whitespace/declaration] [3]',
+                         'foo.c')
+        self.assert_lint('int& b;', '', 'foo.cpp')
+        self.assert_lint('int &b;',
+                         'Declaration has space between type name and & in int &b  [whitespace/declaration] [3]',
+                         'foo.cpp')
+        self.assert_lint('return &b;', '', 'foo.cpp')
+
+    def test_indent(self):
+        self.assert_lint('static int noindent;', '')
+        self.assert_lint('    int fourSpaceIndent;', '')
+        self.assert_lint(' int oneSpaceIndent;',
+                         'Weird number of spaces at line-start.  '
+                         'Are you using a 4-space indent?  [whitespace/indent] [3]')
+        self.assert_lint('   int threeSpaceIndent;',
+                         'Weird number of spaces at line-start.  '
+                         'Are you using a 4-space indent?  [whitespace/indent] [3]')
+        self.assert_lint(' char* oneSpaceIndent = "public:";',
+                         'Weird number of spaces at line-start.  '
+                         'Are you using a 4-space indent?  [whitespace/indent] [3]')
+        self.assert_lint(' public:',
+                         'Weird number of spaces at line-start.  '
+                         'Are you using a 4-space indent?  [whitespace/indent] [3]')
+        self.assert_lint('  public:',
+                         'Weird number of spaces at line-start.  '
+                         'Are you using a 4-space indent?  [whitespace/indent] [3]')
+        self.assert_lint('   public:',
+                         'Weird number of spaces at line-start.  '
+                         'Are you using a 4-space indent?  [whitespace/indent] [3]')
+        self.assert_multi_line_lint(
+            'class Foo {\n'
+            'public:\n'
+            '    enum Bar {\n'
+            '        Alpha,\n'
+            '        Beta,\n'
+            '#if ENABLED_BETZ\n'
+            '        Charlie,\n'
+            '#endif\n'
+            '    };\n'
+            '};',
+            '')
+
+    def test_not_alabel(self):
+        self.assert_lint('MyVeryLongNamespace::MyVeryLongClassName::', '')
+
+    def test_tab(self):
+        self.assert_lint('\tint a;',
+                         'Tab found; better to use spaces  [whitespace/tab] [1]')
+        self.assert_lint('int a = 5;\t// set a to 5',
+                         'Tab found; better to use spaces  [whitespace/tab] [1]')
+
+    def test_unnamed_namespaces_in_headers(self):
+        self.assert_language_rules_check(
+            'foo.h', 'namespace {',
+            'Do not use unnamed namespaces in header files.  See'
+            ' http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Namespaces'
+            ' for more information.  [build/namespaces] [4]')
+        # namespace registration macros are OK.
+        self.assert_language_rules_check('foo.h', 'namespace {  \\', '')
+        # named namespaces are OK.
+        self.assert_language_rules_check('foo.h', 'namespace foo {', '')
+        self.assert_language_rules_check('foo.h', 'namespace foonamespace {', '')
+        self.assert_language_rules_check('foo.cpp', 'namespace {', '')
+        self.assert_language_rules_check('foo.cpp', 'namespace foo {', '')
+
+    def test_build_class(self):
+        # Test that the linter can parse to the end of class definitions,
+        # and that it will report when it can't.
+        # Use multi-line linter because it performs the ClassState check.
+        self.assert_multi_line_lint(
+            'class Foo {',
+            'Failed to find complete declaration of class Foo'
+            '  [build/class] [5]')
+        # Don't warn on forward declarations of various types.
+        self.assert_multi_line_lint(
+            'class Foo;',
+            '')
+        self.assert_multi_line_lint(
+            '''\
+            struct Foo*
+                foo = NewFoo();''',
+            '')
+        # Here is an example where the linter gets confused, even though
+        # the code doesn't violate the style guide.
+        self.assert_multi_line_lint(
+            'class Foo\n'
+            '#ifdef DERIVE_FROM_GOO\n'
+            '    : public Goo {\n'
+            '#else\n'
+            '    : public Hoo {\n'
+            '#endif\n'
+            '};',
+            'Failed to find complete declaration of class Foo'
+            '  [build/class] [5]')
+
+    def test_build_end_comment(self):
+        # The crosstool compiler we currently use will fail to compile the
+        # code in this test, so we might consider removing the lint check.
+        self.assert_lint('#endif Not a comment',
+                         'Uncommented text after #endif is non-standard.'
+                         '  Use a comment.'
+                         '  [build/endif_comment] [5]')
+
+    def test_build_forward_decl(self):
+        # The crosstool compiler we currently use will fail to compile the
+        # code in this test, so we might consider removing the lint check.
+        self.assert_lint('class Foo::Goo;',
+                         'Inner-style forward declarations are invalid.'
+                         '  Remove this line.'
+                         '  [build/forward_decl] [5]')
+
+    def test_build_header_guard(self):
+        file_path = 'mydir/Foo.h'
+
+        # We can't rely on our internal stuff to get a sane path on the open source
+        # side of things, so just parse out the suggested header guard. This
+        # doesn't allow us to test the suggested header guard, but it does let us
+        # test all the other header tests.
+        error_collector = ErrorCollector(self.assert_)
+        self.process_file_data(file_path, 'h', [], error_collector)
+        expected_guard = ''
+        matcher = re.compile(
+            'No \#ifndef header guard found\, suggested CPP variable is\: ([A-Za-z_0-9]+) ')
+        for error in error_collector.result_list():
+            matches = matcher.match(error)
+            if matches:
+                expected_guard = matches.group(1)
+                break
+
+        # Make sure we extracted something for our header guard.
+        self.assertNotEqual(expected_guard, '')
+
+        # Wrong guard
+        error_collector = ErrorCollector(self.assert_)
+        self.process_file_data(file_path, 'h',
+                               ['#ifndef FOO_H', '#define FOO_H'], error_collector)
+        self.assertEquals(
+            1,
+            error_collector.result_list().count(
+                '#ifndef header guard has wrong style, please use: %s'
+                '  [build/header_guard] [5]' % expected_guard),
+            error_collector.result_list())
+
+        # No define
+        error_collector = ErrorCollector(self.assert_)
+        self.process_file_data(file_path, 'h',
+                               ['#ifndef %s' % expected_guard], error_collector)
+        self.assertEquals(
+            1,
+            error_collector.result_list().count(
+                'No #ifndef header guard found, suggested CPP variable is: %s'
+                '  [build/header_guard] [5]' % expected_guard),
+            error_collector.result_list())
+
+        # Mismatched define
+        error_collector = ErrorCollector(self.assert_)
+        self.process_file_data(file_path, 'h',
+                               ['#ifndef %s' % expected_guard,
+                                '#define FOO_H'],
+                               error_collector)
+        self.assertEquals(
+            1,
+            error_collector.result_list().count(
+                'No #ifndef header guard found, suggested CPP variable is: %s'
+                '  [build/header_guard] [5]' % expected_guard),
+            error_collector.result_list())
+
+        # No header guard errors
+        error_collector = ErrorCollector(self.assert_)
+        self.process_file_data(file_path, 'h',
+                               ['#ifndef %s' % expected_guard,
+                                '#define %s' % expected_guard,
+                                '#endif // %s' % expected_guard],
+                               error_collector)
+        for line in error_collector.result_list():
+            if line.find('build/header_guard') != -1:
+                self.fail('Unexpected error: %s' % line)
+
+        # Completely incorrect header guard
+        error_collector = ErrorCollector(self.assert_)
+        self.process_file_data(file_path, 'h',
+                               ['#ifndef FOO',
+                                '#define FOO',
+                                '#endif  // FOO'],
+                               error_collector)
+        self.assertEquals(
+            1,
+            error_collector.result_list().count(
+                '#ifndef header guard has wrong style, please use: %s'
+                '  [build/header_guard] [5]' % expected_guard),
+            error_collector.result_list())
+
+        # Special case for flymake
+        error_collector = ErrorCollector(self.assert_)
+        self.process_file_data('mydir/Foo_flymake.h', 'h',
+                               ['#ifndef %s' % expected_guard,
+                                '#define %s' % expected_guard,
+                                '#endif // %s' % expected_guard],
+                               error_collector)
+        for line in error_collector.result_list():
+            if line.find('build/header_guard') != -1:
+                self.fail('Unexpected error: %s' % line)
+
+        error_collector = ErrorCollector(self.assert_)
+        self.process_file_data('mydir/Foo_flymake.h', 'h', [], error_collector)
+        self.assertEquals(
+            1,
+            error_collector.result_list().count(
+                'No #ifndef header guard found, suggested CPP variable is: %s'
+                '  [build/header_guard] [5]' % expected_guard),
+            error_collector.result_list())
+
+        # Verify that we don't blindly suggest the WTF prefix for all headers.
+        self.assertFalse(expected_guard.startswith('WTF_'))
+
+        # Allow the WTF_ prefix for files in that directory.
+        header_guard_filter = FilterConfiguration(('-', '+build/header_guard'))
+        error_collector = ErrorCollector(self.assert_, header_guard_filter)
+        self.process_file_data('Source/JavaScriptCore/wtf/TestName.h', 'h',
+                               ['#ifndef WTF_TestName_h', '#define WTF_TestName_h'],
+                               error_collector)
+        self.assertEquals(0, len(error_collector.result_list()),
+                          error_collector.result_list())
+
+        # Also allow the non WTF_ prefix for files in that directory.
+        error_collector = ErrorCollector(self.assert_, header_guard_filter)
+        self.process_file_data('Source/JavaScriptCore/wtf/TestName.h', 'h',
+                               ['#ifndef TestName_h', '#define TestName_h'],
+                               error_collector)
+        self.assertEquals(0, len(error_collector.result_list()),
+                          error_collector.result_list())
+
+        # Verify that we suggest the WTF prefix version.
+        error_collector = ErrorCollector(self.assert_, header_guard_filter)
+        self.process_file_data('Source/JavaScriptCore/wtf/TestName.h', 'h',
+                               ['#ifndef BAD_TestName_h', '#define BAD_TestName_h'],
+                               error_collector)
+        self.assertEquals(
+            1,
+            error_collector.result_list().count(
+                '#ifndef header guard has wrong style, please use: WTF_TestName_h'
+                '  [build/header_guard] [5]'),
+            error_collector.result_list())
+
+    def test_build_printf_format(self):
+        self.assert_lint(
+            r'printf("\%%d", value);',
+            '%, [, (, and { are undefined character escapes.  Unescape them.'
+            '  [build/printf_format] [3]')
+
+        self.assert_lint(
+            r'snprintf(buffer, sizeof(buffer), "\[%d", value);',
+            '%, [, (, and { are undefined character escapes.  Unescape them.'
+            '  [build/printf_format] [3]')
+
+        self.assert_lint(
+            r'fprintf(file, "\(%d", value);',
+            '%, [, (, and { are undefined character escapes.  Unescape them.'
+            '  [build/printf_format] [3]')
+
+        self.assert_lint(
+            r'vsnprintf(buffer, sizeof(buffer), "\\\{%d", ap);',
+            '%, [, (, and { are undefined character escapes.  Unescape them.'
+            '  [build/printf_format] [3]')
+
+        # Don't warn if double-slash precedes the symbol
+        self.assert_lint(r'printf("\\%%%d", value);',
+                         '')
+
+    def test_runtime_printf_format(self):
+        self.assert_lint(
+            r'fprintf(file, "%q", value);',
+            '%q in format strings is deprecated.  Use %ll instead.'
+            '  [runtime/printf_format] [3]')
+
+        self.assert_lint(
+            r'aprintf(file, "The number is %12q", value);',
+            '%q in format strings is deprecated.  Use %ll instead.'
+            '  [runtime/printf_format] [3]')
+
+        self.assert_lint(
+            r'printf(file, "The number is" "%-12q", value);',
+            '%q in format strings is deprecated.  Use %ll instead.'
+            '  [runtime/printf_format] [3]')
+
+        self.assert_lint(
+            r'printf(file, "The number is" "%+12q", value);',
+            '%q in format strings is deprecated.  Use %ll instead.'
+            '  [runtime/printf_format] [3]')
+
+        self.assert_lint(
+            r'printf(file, "The number is" "% 12q", value);',
+            '%q in format strings is deprecated.  Use %ll instead.'
+            '  [runtime/printf_format] [3]')
+
+        self.assert_lint(
+            r'snprintf(file, "Never mix %d and %1$d parmaeters!", value);',
+            '%N$ formats are unconventional.  Try rewriting to avoid them.'
+            '  [runtime/printf_format] [2]')
+
+    def assert_lintLogCodeOnError(self, code, expected_message):
+        # Special assert_lint which logs the input code on error.
+        result = self.perform_single_line_lint(code, 'foo.cpp')
+        if result != expected_message:
+            self.fail('For code: "%s"\nGot: "%s"\nExpected: "%s"'
+                      % (code, result, expected_message))
+
+    def test_build_storage_class(self):
+        qualifiers = [None, 'const', 'volatile']
+        signs = [None, 'signed', 'unsigned']
+        types = ['void', 'char', 'int', 'float', 'double',
+                 'schar', 'int8', 'uint8', 'int16', 'uint16',
+                 'int32', 'uint32', 'int64', 'uint64']
+        storage_classes = ['auto', 'extern', 'register', 'static', 'typedef']
+
+        build_storage_class_error_message = (
+            'Storage class (static, extern, typedef, etc) should be first.'
+            '  [build/storage_class] [5]')
+
+        # Some explicit cases. Legal in C++, deprecated in C99.
+        self.assert_lint('const int static foo = 5;',
+                         build_storage_class_error_message)
+
+        self.assert_lint('char static foo;',
+                         build_storage_class_error_message)
+
+        self.assert_lint('double const static foo = 2.0;',
+                         build_storage_class_error_message)
+
+        self.assert_lint('uint64 typedef unsignedLongLong;',
+                         build_storage_class_error_message)
+
+        self.assert_lint('int register foo = 0;',
+                         build_storage_class_error_message)
+
+        # Since there are a very large number of possibilities, randomly
+        # construct declarations.
+        # Make sure that the declaration is logged if there's an error.
+        # Seed generator with an integer for absolute reproducibility.
+        random.seed(25)
+        for unused_i in range(10):
+            # Build up random list of non-storage-class declaration specs.
+            other_decl_specs = [random.choice(qualifiers), random.choice(signs),
+                                random.choice(types)]
+            # remove None
+            other_decl_specs = filter(lambda x: x is not None, other_decl_specs)
+
+            # shuffle
+            random.shuffle(other_decl_specs)
+
+            # insert storage class after the first
+            storage_class = random.choice(storage_classes)
+            insertion_point = random.randint(1, len(other_decl_specs))
+            decl_specs = (other_decl_specs[0:insertion_point]
+                          + [storage_class]
+                          + other_decl_specs[insertion_point:])
+
+            self.assert_lintLogCodeOnError(
+                ' '.join(decl_specs) + ';',
+                build_storage_class_error_message)
+
+            # but no error if storage class is first
+            self.assert_lintLogCodeOnError(
+                storage_class + ' ' + ' '.join(other_decl_specs),
+                '')
+
+    def test_legal_copyright(self):
+        legal_copyright_message = (
+            'No copyright message found.  '
+            'You should have a line: "Copyright [year] <Copyright Owner>"'
+            '  [legal/copyright] [5]')
+
+        copyright_line = '// Copyright 2008 Google Inc. All Rights Reserved.'
+
+        file_path = 'mydir/googleclient/foo.cpp'
+
+        # There should be a copyright message in the first 10 lines
+        error_collector = ErrorCollector(self.assert_)
+        self.process_file_data(file_path, 'cpp', [], error_collector)
+        self.assertEquals(
+            1,
+            error_collector.result_list().count(legal_copyright_message))
+
+        error_collector = ErrorCollector(self.assert_)
+        self.process_file_data(
+            file_path, 'cpp',
+            ['' for unused_i in range(10)] + [copyright_line],
+            error_collector)
+        self.assertEquals(
+            1,
+            error_collector.result_list().count(legal_copyright_message))
+
+        # Test that warning isn't issued if Copyright line appears early enough.
+        error_collector = ErrorCollector(self.assert_)
+        self.process_file_data(file_path, 'cpp', [copyright_line], error_collector)
+        for message in error_collector.result_list():
+            if message.find('legal/copyright') != -1:
+                self.fail('Unexpected error: %s' % message)
+
+        error_collector = ErrorCollector(self.assert_)
+        self.process_file_data(
+            file_path, 'cpp',
+            ['' for unused_i in range(9)] + [copyright_line],
+            error_collector)
+        for message in error_collector.result_list():
+            if message.find('legal/copyright') != -1:
+                self.fail('Unexpected error: %s' % message)
+
+    def test_invalid_increment(self):
+        self.assert_lint('*count++;',
+                         'Changing pointer instead of value (or unused value of '
+                         'operator*).  [runtime/invalid_increment] [5]')
+
+    # Integral bitfields must be declared with either signed or unsigned keyword.
+    def test_plain_integral_bitfields(self):
+        errmsg = ('Please declare integral type bitfields with either signed or unsigned.  [runtime/bitfields] [5]')
+
+        self.assert_lint('int a : 30;', errmsg)
+        self.assert_lint('mutable short a : 14;', errmsg)
+        self.assert_lint('const char a : 6;', errmsg)
+        self.assert_lint('long int a : 30;', errmsg)
+        self.assert_lint('int a = 1 ? 0 : 30;', '')
+
+class CleansedLinesTest(unittest.TestCase):
+    def test_init(self):
+        lines = ['Line 1',
+                 'Line 2',
+                 'Line 3 // Comment test',
+                 'Line 4 "foo"']
+
+        clean_lines = cpp_style.CleansedLines(lines)
+        self.assertEquals(lines, clean_lines.raw_lines)
+        self.assertEquals(4, clean_lines.num_lines())
+
+        self.assertEquals(['Line 1',
+                           'Line 2',
+                           'Line 3 ',
+                           'Line 4 "foo"'],
+                          clean_lines.lines)
+
+        self.assertEquals(['Line 1',
+                           'Line 2',
+                           'Line 3 ',
+                           'Line 4 ""'],
+                          clean_lines.elided)
+
+    def test_init_empty(self):
+        clean_lines = cpp_style.CleansedLines([])
+        self.assertEquals([], clean_lines.raw_lines)
+        self.assertEquals(0, clean_lines.num_lines())
+
+    def test_collapse_strings(self):
+        collapse = cpp_style.CleansedLines.collapse_strings
+        self.assertEquals('""', collapse('""'))             # ""     (empty)
+        self.assertEquals('"""', collapse('"""'))           # """    (bad)
+        self.assertEquals('""', collapse('"xyz"'))          # "xyz"  (string)
+        self.assertEquals('""', collapse('"\\\""'))         # "\""   (string)
+        self.assertEquals('""', collapse('"\'"'))           # "'"    (string)
+        self.assertEquals('"\"', collapse('"\"'))           # "\"    (bad)
+        self.assertEquals('""', collapse('"\\\\"'))         # "\\"   (string)
+        self.assertEquals('"', collapse('"\\\\\\"'))        # "\\\"  (bad)
+        self.assertEquals('""', collapse('"\\\\\\\\"'))     # "\\\\" (string)
+
+        self.assertEquals('\'\'', collapse('\'\''))         # ''     (empty)
+        self.assertEquals('\'\'', collapse('\'a\''))        # 'a'    (char)
+        self.assertEquals('\'\'', collapse('\'\\\'\''))     # '\''   (char)
+        self.assertEquals('\'', collapse('\'\\\''))         # '\'    (bad)
+        self.assertEquals('', collapse('\\012'))            # '\012' (char)
+        self.assertEquals('', collapse('\\xfF0'))           # '\xfF0' (char)
+        self.assertEquals('', collapse('\\n'))              # '\n' (char)
+        self.assertEquals('\#', collapse('\\#'))            # '\#' (bad)
+
+        self.assertEquals('StringReplace(body, "", "");',
+                          collapse('StringReplace(body, "\\\\", "\\\\\\\\");'))
+        self.assertEquals('\'\' ""',
+                          collapse('\'"\' "foo"'))
+
+
+class OrderOfIncludesTest(CppStyleTestBase):
+    def setUp(self):
+        self.include_state = cpp_style._IncludeState()
+
+        # Cheat os.path.abspath called in FileInfo class.
+        self.os_path_abspath_orig = os.path.abspath
+        os.path.abspath = lambda value: value
+
+    def tearDown(self):
+        os.path.abspath = self.os_path_abspath_orig
+
+    def test_try_drop_common_suffixes(self):
+        self.assertEqual('foo/foo', cpp_style._drop_common_suffixes('foo/foo-inl.h'))
+        self.assertEqual('foo/bar/foo',
+                         cpp_style._drop_common_suffixes('foo/bar/foo_inl.h'))
+        self.assertEqual('foo/foo', cpp_style._drop_common_suffixes('foo/foo.cpp'))
+        self.assertEqual('foo/foo_unusualinternal',
+                         cpp_style._drop_common_suffixes('foo/foo_unusualinternal.h'))
+        self.assertEqual('',
+                         cpp_style._drop_common_suffixes('_test.cpp'))
+        self.assertEqual('test',
+                         cpp_style._drop_common_suffixes('test.cpp'))
+
+
+class OrderOfIncludesTest(CppStyleTestBase):
+    def setUp(self):
+        self.include_state = cpp_style._IncludeState()
+
+        # Cheat os.path.abspath called in FileInfo class.
+        self.os_path_abspath_orig = os.path.abspath
+        self.os_path_isfile_orig = os.path.isfile
+        os.path.abspath = lambda value: value
+
+    def tearDown(self):
+        os.path.abspath = self.os_path_abspath_orig
+        os.path.isfile = self.os_path_isfile_orig
+
+    def test_check_next_include_order__no_config(self):
+        self.assertEqual('Header file should not contain WebCore config.h.',
+                         self.include_state.check_next_include_order(cpp_style._CONFIG_HEADER, True, True))
+
+    def test_check_next_include_order__no_self(self):
+        self.assertEqual('Header file should not contain itself.',
+                         self.include_state.check_next_include_order(cpp_style._PRIMARY_HEADER, True, True))
+        # Test actual code to make sure that header types are correctly assigned.
+        self.assert_language_rules_check('Foo.h',
+                                         '#include "Foo.h"\n',
+                                         'Header file should not contain itself. Should be: alphabetically sorted.'
+                                         '  [build/include_order] [4]')
+        self.assert_language_rules_check('FooBar.h',
+                                         '#include "Foo.h"\n',
+                                         '')
+
+    def test_check_next_include_order__likely_then_config(self):
+        self.assertEqual('Found header this file implements before WebCore config.h.',
+                         self.include_state.check_next_include_order(cpp_style._PRIMARY_HEADER, False, True))
+        self.assertEqual('Found WebCore config.h after a header this file implements.',
+                         self.include_state.check_next_include_order(cpp_style._CONFIG_HEADER, False, True))
+
+    def test_check_next_include_order__other_then_config(self):
+        self.assertEqual('Found other header before WebCore config.h.',
+                         self.include_state.check_next_include_order(cpp_style._OTHER_HEADER, False, True))
+        self.assertEqual('Found WebCore config.h after other header.',
+                         self.include_state.check_next_include_order(cpp_style._CONFIG_HEADER, False, True))
+
+    def test_check_next_include_order__config_then_other_then_likely(self):
+        self.assertEqual('', self.include_state.check_next_include_order(cpp_style._CONFIG_HEADER, False, True))
+        self.assertEqual('Found other header before a header this file implements.',
+                         self.include_state.check_next_include_order(cpp_style._OTHER_HEADER, False, True))
+        self.assertEqual('Found header this file implements after other header.',
+                         self.include_state.check_next_include_order(cpp_style._PRIMARY_HEADER, False, True))
+
+    def test_check_alphabetical_include_order(self):
+        self.assert_language_rules_check('foo.h',
+                                         '#include "a.h"\n'
+                                         '#include "c.h"\n'
+                                         '#include "b.h"\n',
+                                         'Alphabetical sorting problem.  [build/include_order] [4]')
+
+        self.assert_language_rules_check('foo.h',
+                                         '#include "a.h"\n'
+                                         '#include "b.h"\n'
+                                         '#include "c.h"\n',
+                                         '')
+
+        self.assert_language_rules_check('foo.h',
+                                         '#include <assert.h>\n'
+                                         '#include "bar.h"\n',
+                                         'Alphabetical sorting problem.  [build/include_order] [4]')
+
+        self.assert_language_rules_check('foo.h',
+                                         '#include "bar.h"\n'
+                                         '#include <assert.h>\n',
+                                         '')
+
+    def test_check_alphabetical_include_order_errors_reported_for_both_lines(self):
+        # If one of the two lines of out of order headers are filtered, the error should be
+        # reported on the other line.
+        self.assert_language_rules_check('foo.h',
+                                         '#include "a.h"\n'
+                                         '#include "c.h"\n'
+                                         '#include "b.h"\n',
+                                         'Alphabetical sorting problem.  [build/include_order] [4]',
+                                         lines_to_check=[2])
+
+        self.assert_language_rules_check('foo.h',
+                                         '#include "a.h"\n'
+                                         '#include "c.h"\n'
+                                         '#include "b.h"\n',
+                                         'Alphabetical sorting problem.  [build/include_order] [4]',
+                                         lines_to_check=[3])
+
+        # If no lines are filtered, the error should be reported only once.
+        self.assert_language_rules_check('foo.h',
+                                         '#include "a.h"\n'
+                                         '#include "c.h"\n'
+                                         '#include "b.h"\n',
+                                         'Alphabetical sorting problem.  [build/include_order] [4]')
+
+    def test_check_line_break_after_own_header(self):
+        self.assert_language_rules_check('foo.cpp',
+                                         '#include "config.h"\n'
+                                         '#include "foo.h"\n'
+                                         '#include "bar.h"\n',
+                                         'You should add a blank line after implementation file\'s own header.  [build/include_order] [4]')
+
+        self.assert_language_rules_check('foo.cpp',
+                                         '#include "config.h"\n'
+                                         '#include "foo.h"\n'
+                                         '\n'
+                                         '#include "bar.h"\n',
+                                         '')
+
+    def test_check_preprocessor_in_include_section(self):
+        self.assert_language_rules_check('foo.cpp',
+                                         '#include "config.h"\n'
+                                         '#include "foo.h"\n'
+                                         '\n'
+                                         '#ifdef BAZ\n'
+                                         '#include "baz.h"\n'
+                                         '#else\n'
+                                         '#include "foobar.h"\n'
+                                         '#endif"\n'
+                                         '#include "bar.h"\n', # No flag because previous is in preprocessor section
+                                         '')
+
+        self.assert_language_rules_check('foo.cpp',
+                                         '#include "config.h"\n'
+                                         '#include "foo.h"\n'
+                                         '\n'
+                                         '#ifdef BAZ\n'
+                                         '#include "baz.h"\n'
+                                         '#endif"\n'
+                                         '#include "bar.h"\n'
+                                         '#include "a.h"\n', # Should still flag this.
+                                         'Alphabetical sorting problem.  [build/include_order] [4]')
+
+        self.assert_language_rules_check('foo.cpp',
+                                         '#include "config.h"\n'
+                                         '#include "foo.h"\n'
+                                         '\n'
+                                         '#ifdef BAZ\n'
+                                         '#include "baz.h"\n'
+                                         '#include "bar.h"\n' #Should still flag this
+                                         '#endif"\n',
+                                         'Alphabetical sorting problem.  [build/include_order] [4]')
+
+        self.assert_language_rules_check('foo.cpp',
+                                         '#include "config.h"\n'
+                                         '#include "foo.h"\n'
+                                         '\n'
+                                         '#ifdef BAZ\n'
+                                         '#include "baz.h"\n'
+                                         '#endif"\n'
+                                         '#ifdef FOOBAR\n'
+                                         '#include "foobar.h"\n'
+                                         '#endif"\n'
+                                         '#include "bar.h"\n'
+                                         '#include "a.h"\n', # Should still flag this.
+                                         'Alphabetical sorting problem.  [build/include_order] [4]')
+
+        # Check that after an already included error, the sorting rules still work.
+        self.assert_language_rules_check('foo.cpp',
+                                         '#include "config.h"\n'
+                                         '#include "foo.h"\n'
+                                         '\n'
+                                         '#include "foo.h"\n'
+                                         '#include "g.h"\n',
+                                         '"foo.h" already included at foo.cpp:2  [build/include] [4]')
+
+    def test_primary_header(self):
+        # File with non-existing primary header should not produce errors.
+        self.assert_language_rules_check('foo.cpp',
+                                         '#include "config.h"\n'
+                                         '\n'
+                                         '#include "bar.h"\n',
+                                         '')
+        # Pretend that header files exist.
+        os.path.isfile = lambda filename: True
+        # Missing include for existing primary header -> error.
+        self.assert_language_rules_check('foo.cpp',
+                                         '#include "config.h"\n'
+                                         '\n'
+                                         '#include "bar.h"\n',
+                                         'Found other header before a header this file implements. '
+                                         'Should be: config.h, primary header, blank line, and then '
+                                         'alphabetically sorted.  [build/include_order] [4]')
+        # Having include for existing primary header -> no error.
+        self.assert_language_rules_check('foo.cpp',
+                                         '#include "config.h"\n'
+                                         '#include "foo.h"\n'
+                                         '\n'
+                                         '#include "bar.h"\n',
+                                         '')
+
+        os.path.isfile = self.os_path_isfile_orig
+
+    def test_public_primary_header(self):
+        # System header is not considered a primary header.
+        self.assert_language_rules_check('foo.cpp',
+                                         '#include "config.h"\n'
+                                         '#include <other/foo.h>\n'
+                                         '\n'
+                                         '#include "a.h"\n',
+                                         'Alphabetical sorting problem.  [build/include_order] [4]')
+
+        # ...except that it starts with public/.
+        self.assert_language_rules_check('foo.cpp',
+                                         '#include "config.h"\n'
+                                         '#include <public/foo.h>\n'
+                                         '\n'
+                                         '#include "a.h"\n',
+                                         '')
+
+        # Even if it starts with public/ its base part must match with the source file name.
+        self.assert_language_rules_check('foo.cpp',
+                                         '#include "config.h"\n'
+                                         '#include <public/foop.h>\n'
+                                         '\n'
+                                         '#include "a.h"\n',
+                                         'Alphabetical sorting problem.  [build/include_order] [4]')
+
+    def test_check_wtf_includes(self):
+        self.assert_language_rules_check('foo.cpp',
+                                         '#include "config.h"\n'
+                                         '#include "foo.h"\n'
+                                         '\n'
+                                         '#include <wtf/Assertions.h>\n',
+                                         '')
+        self.assert_language_rules_check('foo.cpp',
+                                         '#include "config.h"\n'
+                                         '#include "foo.h"\n'
+                                         '\n'
+                                         '#include "wtf/Assertions.h"\n',
+                                         'wtf includes should be <wtf/file.h> instead of "wtf/file.h".'
+                                         '  [build/include] [4]')
+
+    def test_check_cc_includes(self):
+        self.assert_language_rules_check('bar/chromium/foo.cpp',
+                                         '#include "config.h"\n'
+                                         '#include "foo.h"\n'
+                                         '\n'
+                                         '#include "cc/CCProxy.h"\n',
+                                         'cc includes should be "CCFoo.h" instead of "cc/CCFoo.h".'
+                                         '  [build/include] [4]')
+
+    def test_classify_include(self):
+        classify_include = cpp_style._classify_include
+        include_state = cpp_style._IncludeState()
+        self.assertEqual(cpp_style._CONFIG_HEADER,
+                         classify_include('foo/foo.cpp',
+                                          'config.h',
+                                          False, include_state))
+        self.assertEqual(cpp_style._PRIMARY_HEADER,
+                         classify_include('foo/internal/foo.cpp',
+                                          'foo/public/foo.h',
+                                          False, include_state))
+        self.assertEqual(cpp_style._PRIMARY_HEADER,
+                         classify_include('foo/internal/foo.cpp',
+                                          'foo/other/public/foo.h',
+                                          False, include_state))
+        self.assertEqual(cpp_style._OTHER_HEADER,
+                         classify_include('foo/internal/foo.cpp',
+                                          'foo/other/public/foop.h',
+                                          False, include_state))
+        self.assertEqual(cpp_style._OTHER_HEADER,
+                         classify_include('foo/foo.cpp',
+                                          'string',
+                                          True, include_state))
+        self.assertEqual(cpp_style._PRIMARY_HEADER,
+                         classify_include('fooCustom.cpp',
+                                          'foo.h',
+                                          False, include_state))
+        self.assertEqual(cpp_style._PRIMARY_HEADER,
+                         classify_include('PrefixFooCustom.cpp',
+                                          'Foo.h',
+                                          False, include_state))
+        self.assertEqual(cpp_style._MOC_HEADER,
+                         classify_include('foo.cpp',
+                                          'foo.moc',
+                                          False, include_state))
+        self.assertEqual(cpp_style._MOC_HEADER,
+                         classify_include('foo.cpp',
+                                          'moc_foo.cpp',
+                                          False, include_state))
+        # <public/foo.h> must be considered as primary even if is_system is True.
+        self.assertEqual(cpp_style._PRIMARY_HEADER,
+                         classify_include('foo/foo.cpp',
+                                          'public/foo.h',
+                                          True, include_state))
+        self.assertEqual(cpp_style._OTHER_HEADER,
+                         classify_include('foo.cpp',
+                                          'foo.h',
+                                          True, include_state))
+        self.assertEqual(cpp_style._OTHER_HEADER,
+                         classify_include('foo.cpp',
+                                          'public/foop.h',
+                                          True, include_state))
+        # Qt private APIs use _p.h suffix.
+        self.assertEqual(cpp_style._PRIMARY_HEADER,
+                         classify_include('foo.cpp',
+                                          'foo_p.h',
+                                          False, include_state))
+        # Tricky example where both includes might be classified as primary.
+        self.assert_language_rules_check('ScrollbarThemeWince.cpp',
+                                         '#include "config.h"\n'
+                                         '#include "ScrollbarThemeWince.h"\n'
+                                         '\n'
+                                         '#include "Scrollbar.h"\n',
+                                         '')
+        self.assert_language_rules_check('ScrollbarThemeWince.cpp',
+                                         '#include "config.h"\n'
+                                         '#include "Scrollbar.h"\n'
+                                         '\n'
+                                         '#include "ScrollbarThemeWince.h"\n',
+                                         'Found header this file implements after a header this file implements.'
+                                         ' Should be: config.h, primary header, blank line, and then alphabetically sorted.'
+                                         '  [build/include_order] [4]')
+        self.assert_language_rules_check('ResourceHandleWin.cpp',
+                                         '#include "config.h"\n'
+                                         '#include "ResourceHandle.h"\n'
+                                         '\n'
+                                         '#include "ResourceHandleWin.h"\n',
+                                         '')
+
+    def test_try_drop_common_suffixes(self):
+        self.assertEqual('foo/foo', cpp_style._drop_common_suffixes('foo/foo-inl.h'))
+        self.assertEqual('foo/bar/foo',
+                         cpp_style._drop_common_suffixes('foo/bar/foo_inl.h'))
+        self.assertEqual('foo/foo', cpp_style._drop_common_suffixes('foo/foo.cpp'))
+        self.assertEqual('foo/foo_unusualinternal',
+                         cpp_style._drop_common_suffixes('foo/foo_unusualinternal.h'))
+        self.assertEqual('',
+                         cpp_style._drop_common_suffixes('_test.cpp'))
+        self.assertEqual('test',
+                         cpp_style._drop_common_suffixes('test.cpp'))
+        self.assertEqual('test',
+                         cpp_style._drop_common_suffixes('test.cpp'))
+
+class CheckForFunctionLengthsTest(CppStyleTestBase):
+    def setUp(self):
+        # Reducing these thresholds for the tests speeds up tests significantly.
+        self.old_normal_trigger = cpp_style._FunctionState._NORMAL_TRIGGER
+        self.old_test_trigger = cpp_style._FunctionState._TEST_TRIGGER
+
+        cpp_style._FunctionState._NORMAL_TRIGGER = 10
+        cpp_style._FunctionState._TEST_TRIGGER = 25
+
+    def tearDown(self):
+        cpp_style._FunctionState._NORMAL_TRIGGER = self.old_normal_trigger
+        cpp_style._FunctionState._TEST_TRIGGER = self.old_test_trigger
+
+    # FIXME: Eliminate the need for this function.
+    def set_min_confidence(self, min_confidence):
+        """Set new test confidence and return old test confidence."""
+        old_min_confidence = self.min_confidence
+        self.min_confidence = min_confidence
+        return old_min_confidence
+
+    def assert_function_lengths_check(self, code, expected_message):
+        """Check warnings for long function bodies are as expected.
+
+        Args:
+          code: C++ source code expected to generate a warning message.
+          expected_message: Message expected to be generated by the C++ code.
+        """
+        self.assertEquals(expected_message,
+                          self.perform_function_lengths_check(code))
+
+    def trigger_lines(self, error_level):
+        """Return number of lines needed to trigger a function length warning.
+
+        Args:
+          error_level: --v setting for cpp_style.
+
+        Returns:
+          Number of lines needed to trigger a function length warning.
+        """
+        return cpp_style._FunctionState._NORMAL_TRIGGER * 2 ** error_level
+
+    def trigger_test_lines(self, error_level):
+        """Return number of lines needed to trigger a test function length warning.
+
+        Args:
+          error_level: --v setting for cpp_style.
+
+        Returns:
+          Number of lines needed to trigger a test function length warning.
+        """
+        return cpp_style._FunctionState._TEST_TRIGGER * 2 ** error_level
+
+    def assert_function_length_check_definition(self, lines, error_level):
+        """Generate long function definition and check warnings are as expected.
+
+        Args:
+          lines: Number of lines to generate.
+          error_level:  --v setting for cpp_style.
+        """
+        trigger_level = self.trigger_lines(self.min_confidence)
+        self.assert_function_lengths_check(
+            'void test(int x)' + self.function_body(lines),
+            ('Small and focused functions are preferred: '
+             'test() has %d non-comment lines '
+             '(error triggered by exceeding %d lines).'
+             '  [readability/fn_size] [%d]'
+             % (lines, trigger_level, error_level)))
+
+    def assert_function_length_check_definition_ok(self, lines):
+        """Generate shorter function definition and check no warning is produced.
+
+        Args:
+          lines: Number of lines to generate.
+        """
+        self.assert_function_lengths_check(
+            'void test(int x)' + self.function_body(lines),
+            '')
+
+    def assert_function_length_check_at_error_level(self, error_level):
+        """Generate and check function at the trigger level for --v setting.
+
+        Args:
+          error_level: --v setting for cpp_style.
+        """
+        self.assert_function_length_check_definition(self.trigger_lines(error_level),
+                                                     error_level)
+
+    def assert_function_length_check_below_error_level(self, error_level):
+        """Generate and check function just below the trigger level for --v setting.
+
+        Args:
+          error_level: --v setting for cpp_style.
+        """
+        self.assert_function_length_check_definition(self.trigger_lines(error_level) - 1,
+                                                     error_level - 1)
+
+    def assert_function_length_check_above_error_level(self, error_level):
+        """Generate and check function just above the trigger level for --v setting.
+
+        Args:
+          error_level: --v setting for cpp_style.
+        """
+        self.assert_function_length_check_definition(self.trigger_lines(error_level) + 1,
+                                                     error_level)
+
+    def function_body(self, number_of_lines):
+        return ' {\n' + '    this_is_just_a_test();\n' * number_of_lines + '}'
+
+    def function_body_with_blank_lines(self, number_of_lines):
+        return ' {\n' + '    this_is_just_a_test();\n\n' * number_of_lines + '}'
+
+    def function_body_with_no_lints(self, number_of_lines):
+        return ' {\n' + '    this_is_just_a_test();  // NOLINT\n' * number_of_lines + '}'
+
+    # Test line length checks.
+    def test_function_length_check_declaration(self):
+        self.assert_function_lengths_check(
+            'void test();',  # Not a function definition
+            '')
+
+    def test_function_length_check_declaration_with_block_following(self):
+        self.assert_function_lengths_check(
+            ('void test();\n'
+             + self.function_body(66)),  # Not a function definition
+            '')
+
+    def test_function_length_check_class_definition(self):
+        self.assert_function_lengths_check(  # Not a function definition
+            'class Test' + self.function_body(66) + ';',
+            '')
+
+    def test_function_length_check_trivial(self):
+        self.assert_function_lengths_check(
+            'void test() {}',  # Not counted
+            '')
+
+    def test_function_length_check_empty(self):
+        self.assert_function_lengths_check(
+            'void test() {\n}',
+            '')
+
+    def test_function_length_check_definition_below_severity0(self):
+        old_min_confidence = self.set_min_confidence(0)
+        self.assert_function_length_check_definition_ok(self.trigger_lines(0) - 1)
+        self.set_min_confidence(old_min_confidence)
+
+    def test_function_length_check_definition_at_severity0(self):
+        old_min_confidence = self.set_min_confidence(0)
+        self.assert_function_length_check_definition_ok(self.trigger_lines(0))
+        self.set_min_confidence(old_min_confidence)
+
+    def test_function_length_check_definition_above_severity0(self):
+        old_min_confidence = self.set_min_confidence(0)
+        self.assert_function_length_check_above_error_level(0)
+        self.set_min_confidence(old_min_confidence)
+
+    def test_function_length_check_definition_below_severity1v0(self):
+        old_min_confidence = self.set_min_confidence(0)
+        self.assert_function_length_check_below_error_level(1)
+        self.set_min_confidence(old_min_confidence)
+
+    def test_function_length_check_definition_at_severity1v0(self):
+        old_min_confidence = self.set_min_confidence(0)
+        self.assert_function_length_check_at_error_level(1)
+        self.set_min_confidence(old_min_confidence)
+
+    def test_function_length_check_definition_below_severity1(self):
+        self.assert_function_length_check_definition_ok(self.trigger_lines(1) - 1)
+
+    def test_function_length_check_definition_at_severity1(self):
+        self.assert_function_length_check_definition_ok(self.trigger_lines(1))
+
+    def test_function_length_check_definition_above_severity1(self):
+        self.assert_function_length_check_above_error_level(1)
+
+    def test_function_length_check_definition_severity1_plus_indented(self):
+        error_level = 1
+        error_lines = self.trigger_lines(error_level) + 1
+        trigger_level = self.trigger_lines(self.min_confidence)
+        indent_spaces = '    '
+        self.assert_function_lengths_check(
+            re.sub(r'(?m)^(.)', indent_spaces + r'\1',
+                   'void test_indent(int x)\n' + self.function_body(error_lines)),
+            ('Small and focused functions are preferred: '
+             'test_indent() has %d non-comment lines '
+             '(error triggered by exceeding %d lines).'
+             '  [readability/fn_size] [%d]')
+            % (error_lines, trigger_level, error_level))
+
+    def test_function_length_check_definition_severity1_plus_blanks(self):
+        error_level = 1
+        error_lines = self.trigger_lines(error_level) + 1
+        trigger_level = self.trigger_lines(self.min_confidence)
+        self.assert_function_lengths_check(
+            'void test_blanks(int x)' + self.function_body(error_lines),
+            ('Small and focused functions are preferred: '
+             'test_blanks() has %d non-comment lines '
+             '(error triggered by exceeding %d lines).'
+             '  [readability/fn_size] [%d]')
+            % (error_lines, trigger_level, error_level))
+
+    def test_function_length_check_complex_definition_severity1(self):
+        error_level = 1
+        error_lines = self.trigger_lines(error_level) + 1
+        trigger_level = self.trigger_lines(self.min_confidence)
+        self.assert_function_lengths_check(
+            ('my_namespace::my_other_namespace::MyVeryLongTypeName<Type1, bool func(const Element*)>*\n'
+             'my_namespace::my_other_namespace<Type3, Type4>::~MyFunction<Type5<Type6, Type7> >(int arg1, char* arg2)'
+             + self.function_body(error_lines)),
+            ('Small and focused functions are preferred: '
+             'my_namespace::my_other_namespace<Type3, Type4>::~MyFunction<Type5<Type6, Type7> >()'
+             ' has %d non-comment lines '
+             '(error triggered by exceeding %d lines).'
+             '  [readability/fn_size] [%d]')
+            % (error_lines, trigger_level, error_level))
+
+    def test_function_length_check_definition_severity1_for_test(self):
+        error_level = 1
+        error_lines = self.trigger_test_lines(error_level) + 1
+        trigger_level = self.trigger_test_lines(self.min_confidence)
+        self.assert_function_lengths_check(
+            'TEST_F(Test, Mutator)' + self.function_body(error_lines),
+            ('Small and focused functions are preferred: '
+             'TEST_F(Test, Mutator) has %d non-comment lines '
+             '(error triggered by exceeding %d lines).'
+             '  [readability/fn_size] [%d]')
+            % (error_lines, trigger_level, error_level))
+
+    def test_function_length_check_definition_severity1_for_split_line_test(self):
+        error_level = 1
+        error_lines = self.trigger_test_lines(error_level) + 1
+        trigger_level = self.trigger_test_lines(self.min_confidence)
+        self.assert_function_lengths_check(
+            ('TEST_F(GoogleUpdateRecoveryRegistryProtectedTest,\n'
+             '    FixGoogleUpdate_AllValues_MachineApp)'  # note: 4 spaces
+             + self.function_body(error_lines)),
+            ('Small and focused functions are preferred: '
+             'TEST_F(GoogleUpdateRecoveryRegistryProtectedTest, '  # 1 space
+             'FixGoogleUpdate_AllValues_MachineApp) has %d non-comment lines '
+             '(error triggered by exceeding %d lines).'
+             '  [readability/fn_size] [%d]')
+            % (error_lines, trigger_level, error_level))
+
+    def test_function_length_check_definition_severity1_for_bad_test_doesnt_break(self):
+        error_level = 1
+        error_lines = self.trigger_test_lines(error_level) + 1
+        trigger_level = self.trigger_test_lines(self.min_confidence)
+        # Since the function name isn't valid, the function detection algorithm
+        # will skip it, so no error is produced.
+        self.assert_function_lengths_check(
+            ('TEST_F('
+             + self.function_body(error_lines)),
+            '')
+
+    def test_function_length_check_definition_severity1_with_embedded_no_lints(self):
+        error_level = 1
+        error_lines = self.trigger_lines(error_level) + 1
+        trigger_level = self.trigger_lines(self.min_confidence)
+        self.assert_function_lengths_check(
+            'void test(int x)' + self.function_body_with_no_lints(error_lines),
+            ('Small and focused functions are preferred: '
+             'test() has %d non-comment lines '
+             '(error triggered by exceeding %d lines).'
+             '  [readability/fn_size] [%d]')
+            % (error_lines, trigger_level, error_level))
+
+    def test_function_length_check_definition_severity1_with_no_lint(self):
+        self.assert_function_lengths_check(
+            ('void test(int x)' + self.function_body(self.trigger_lines(1))
+             + '  // NOLINT -- long function'),
+            '')
+
+    def test_function_length_check_definition_below_severity2(self):
+        self.assert_function_length_check_below_error_level(2)
+
+    def test_function_length_check_definition_severity2(self):
+        self.assert_function_length_check_at_error_level(2)
+
+    def test_function_length_check_definition_above_severity2(self):
+        self.assert_function_length_check_above_error_level(2)
+
+    def test_function_length_check_definition_below_severity3(self):
+        self.assert_function_length_check_below_error_level(3)
+
+    def test_function_length_check_definition_severity3(self):
+        self.assert_function_length_check_at_error_level(3)
+
+    def test_function_length_check_definition_above_severity3(self):
+        self.assert_function_length_check_above_error_level(3)
+
+    def test_function_length_check_definition_below_severity4(self):
+        self.assert_function_length_check_below_error_level(4)
+
+    def test_function_length_check_definition_severity4(self):
+        self.assert_function_length_check_at_error_level(4)
+
+    def test_function_length_check_definition_above_severity4(self):
+        self.assert_function_length_check_above_error_level(4)
+
+    def test_function_length_check_definition_below_severity5(self):
+        self.assert_function_length_check_below_error_level(5)
+
+    def test_function_length_check_definition_at_severity5(self):
+        self.assert_function_length_check_at_error_level(5)
+
+    def test_function_length_check_definition_above_severity5(self):
+        self.assert_function_length_check_above_error_level(5)
+
+    def test_function_length_check_definition_huge_lines(self):
+        # 5 is the limit
+        self.assert_function_length_check_definition(self.trigger_lines(6), 5)
+
+    def test_function_length_not_determinable(self):
+        # Macro invocation without terminating semicolon.
+        self.assert_function_lengths_check(
+            'MACRO(arg)',
+            '')
+
+        # Macro with underscores
+        self.assert_function_lengths_check(
+            'MACRO_WITH_UNDERSCORES(arg1, arg2, arg3)',
+            '')
+
+        self.assert_function_lengths_check(
+            'NonMacro(arg)',
+            'Lint failed to find start of function body.'
+            '  [readability/fn_size] [5]')
+
+
+class NoNonVirtualDestructorsTest(CppStyleTestBase):
+
+    def test_no_error(self):
+        self.assert_multi_line_lint(
+            '''\
+                class Foo {
+                    virtual ~Foo();
+                    virtual void foo();
+                };''',
+            '')
+
+        self.assert_multi_line_lint(
+            '''\
+                class Foo {
+                    virtual inline ~Foo();
+                    virtual void foo();
+                };''',
+            '')
+
+        self.assert_multi_line_lint(
+            '''\
+                class Foo {
+                    inline virtual ~Foo();
+                    virtual void foo();
+                };''',
+            '')
+
+        self.assert_multi_line_lint(
+            '''\
+                class Foo::Goo {
+                    virtual ~Goo();
+                    virtual void goo();
+                };''',
+            '')
+        self.assert_multi_line_lint(
+            'class Foo { void foo(); };',
+            'More than one command on the same line  [whitespace/newline] [4]')
+        self.assert_multi_line_lint(
+            'class MyClass {\n'
+            '    int getIntValue() { ASSERT(m_ptr); return *m_ptr; }\n'
+            '};\n',
+            '')
+        self.assert_multi_line_lint(
+            'class MyClass {\n'
+            '    int getIntValue()\n'
+            '    {\n'
+            '        ASSERT(m_ptr); return *m_ptr;\n'
+            '    }\n'
+            '};\n',
+            'More than one command on the same line  [whitespace/newline] [4]')
+
+        self.assert_multi_line_lint(
+            '''\
+                class Qualified::Goo : public Foo {
+                    virtual void goo();
+                };''',
+            '')
+
+    def test_no_destructor_when_virtual_needed(self):
+        self.assert_multi_line_lint_re(
+            '''\
+                class Foo {
+                    virtual void foo();
+                };''',
+            'The class Foo probably needs a virtual destructor')
+
+    def test_destructor_non_virtual_when_virtual_needed(self):
+        self.assert_multi_line_lint_re(
+            '''\
+                class Foo {
+                    ~Foo();
+                    virtual void foo();
+                };''',
+            'The class Foo probably needs a virtual destructor')
+
+    def test_no_warn_when_derived(self):
+        self.assert_multi_line_lint(
+            '''\
+                class Foo : public Goo {
+                    virtual void foo();
+                };''',
+            '')
+
+    def test_internal_braces(self):
+        self.assert_multi_line_lint_re(
+            '''\
+                class Foo {
+                    enum Goo {
+                        GOO
+                    };
+                    virtual void foo();
+                };''',
+            'The class Foo probably needs a virtual destructor')
+
+    def test_inner_class_needs_virtual_destructor(self):
+        self.assert_multi_line_lint_re(
+            '''\
+                class Foo {
+                    class Goo {
+                        virtual void goo();
+                    };
+                };''',
+            'The class Goo probably needs a virtual destructor')
+
+    def test_outer_class_needs_virtual_destructor(self):
+        self.assert_multi_line_lint_re(
+            '''\
+                class Foo {
+                    class Goo {
+                    };
+                    virtual void foo();
+                };''',
+            'The class Foo probably needs a virtual destructor')
+
+    def test_qualified_class_needs_virtual_destructor(self):
+        self.assert_multi_line_lint_re(
+            '''\
+                class Qualified::Foo {
+                    virtual void foo();
+                };''',
+            'The class Qualified::Foo probably needs a virtual destructor')
+
+    def test_multi_line_declaration_no_error(self):
+        self.assert_multi_line_lint_re(
+            '''\
+                class Foo
+                    : public Goo {
+                    virtual void foo();
+                };''',
+            '')
+
+    def test_multi_line_declaration_with_error(self):
+        self.assert_multi_line_lint(
+            '''\
+                class Foo
+                {
+                    virtual void foo();
+                };''',
+            ['This { should be at the end of the previous line  '
+             '[whitespace/braces] [4]',
+             'The class Foo probably needs a virtual destructor due to having '
+             'virtual method(s), one declared at line 3.  [runtime/virtual] [4]'])
+
+
+class PassPtrTest(CppStyleTestBase):
+    # For http://webkit.org/coding/RefPtr.html
+
+    def assert_pass_ptr_check(self, code, expected_message):
+        """Check warnings for Pass*Ptr are as expected.
+
+        Args:
+          code: C++ source code expected to generate a warning message.
+          expected_message: Message expected to be generated by the C++ code.
+        """
+        self.assertEquals(expected_message,
+                          self.perform_pass_ptr_check(code))
+
+    def test_pass_ref_ptr_in_function(self):
+        self.assert_pass_ptr_check(
+            'int myFunction()\n'
+            '{\n'
+            '    PassRefPtr<Type1> variable = variable2;\n'
+            '}',
+            'Local variables should never be PassRefPtr (see '
+            'http://webkit.org/coding/RefPtr.html).  [readability/pass_ptr] [5]')
+
+    def test_pass_own_ptr_in_function(self):
+        self.assert_pass_ptr_check(
+            'int myFunction()\n'
+            '{\n'
+            '    PassOwnPtr<Type1> variable = variable2;\n'
+            '}',
+            'Local variables should never be PassOwnPtr (see '
+            'http://webkit.org/coding/RefPtr.html).  [readability/pass_ptr] [5]')
+
+    def test_pass_other_type_ptr_in_function(self):
+        self.assert_pass_ptr_check(
+            'int myFunction()\n'
+            '{\n'
+            '    PassOtherTypePtr<Type1> variable;\n'
+            '}',
+            'Local variables should never be PassOtherTypePtr (see '
+            'http://webkit.org/coding/RefPtr.html).  [readability/pass_ptr] [5]')
+
+    def test_pass_ref_ptr_return_value(self):
+        self.assert_pass_ptr_check(
+            'PassRefPtr<Type1>\n'
+            'myFunction(int)\n'
+            '{\n'
+            '}',
+            '')
+        self.assert_pass_ptr_check(
+            'PassRefPtr<Type1> myFunction(int)\n'
+            '{\n'
+            '}',
+            '')
+        self.assert_pass_ptr_check(
+            'PassRefPtr<Type1> myFunction();\n',
+            '')
+        self.assert_pass_ptr_check(
+            'OwnRefPtr<Type1> myFunction();\n',
+            '')
+        self.assert_pass_ptr_check(
+            'RefPtr<Type1> myFunction(int)\n'
+            '{\n'
+            '}',
+            'The return type should use PassRefPtr instead of RefPtr.  [readability/pass_ptr] [5]')
+        self.assert_pass_ptr_check(
+            'OwnPtr<Type1> myFunction(int)\n'
+            '{\n'
+            '}',
+            'The return type should use PassOwnPtr instead of OwnPtr.  [readability/pass_ptr] [5]')
+
+    def test_ref_ptr_parameter_value(self):
+        self.assert_pass_ptr_check(
+            'int myFunction(PassRefPtr<Type1>)\n'
+            '{\n'
+            '}',
+            '')
+        self.assert_pass_ptr_check(
+            'int myFunction(RefPtr<Type1>)\n'
+            '{\n'
+            '}',
+            'The parameter type should use PassRefPtr instead of RefPtr.  [readability/pass_ptr] [5]')
+        self.assert_pass_ptr_check(
+            'int myFunction(RefPtr<Type1>&)\n'
+            '{\n'
+            '}',
+            '')
+        self.assert_pass_ptr_check(
+            'int myFunction(RefPtr<Type1>*)\n'
+            '{\n'
+            '}',
+            '')
+
+    def test_own_ptr_parameter_value(self):
+        self.assert_pass_ptr_check(
+            'int myFunction(PassOwnPtr<Type1>)\n'
+            '{\n'
+            '}',
+            '')
+        self.assert_pass_ptr_check(
+            'int myFunction(OwnPtr<Type1>)\n'
+            '{\n'
+            '}',
+            'The parameter type should use PassOwnPtr instead of OwnPtr.  [readability/pass_ptr] [5]')
+        self.assert_pass_ptr_check(
+            'int myFunction(OwnPtr<Type1>& simple)\n'
+            '{\n'
+            '}',
+            '')
+
+    def test_ref_ptr_member_variable(self):
+        self.assert_pass_ptr_check(
+            'class Foo {'
+            '    RefPtr<Type1> m_other;\n'
+            '};\n',
+            '')
+
+
+class LeakyPatternTest(CppStyleTestBase):
+
+    def assert_leaky_pattern_check(self, code, expected_message):
+        """Check warnings for leaky patterns are as expected.
+
+        Args:
+          code: C++ source code expected to generate a warning message.
+          expected_message: Message expected to be generated by the C++ code.
+        """
+        self.assertEquals(expected_message,
+                          self.perform_leaky_pattern_check(code))
+
+    def test_get_dc(self):
+        self.assert_leaky_pattern_check(
+            'HDC hdc = GetDC(hwnd);',
+            'Use the class HWndDC instead of calling GetDC to avoid potential '
+            'memory leaks.  [runtime/leaky_pattern] [5]')
+
+    def test_get_dc(self):
+        self.assert_leaky_pattern_check(
+            'HDC hdc = GetDCEx(hwnd, 0, 0);',
+            'Use the class HWndDC instead of calling GetDCEx to avoid potential '
+            'memory leaks.  [runtime/leaky_pattern] [5]')
+
+    def test_own_get_dc(self):
+        self.assert_leaky_pattern_check(
+            'HWndDC hdc(hwnd);',
+            '')
+
+    def test_create_dc(self):
+        self.assert_leaky_pattern_check(
+            'HDC dc2 = ::CreateDC();',
+            'Use adoptPtr and OwnPtr<HDC> when calling CreateDC to avoid potential '
+            'memory leaks.  [runtime/leaky_pattern] [5]')
+
+        self.assert_leaky_pattern_check(
+            'adoptPtr(CreateDC());',
+            '')
+
+    def test_create_compatible_dc(self):
+        self.assert_leaky_pattern_check(
+            'HDC dc2 = CreateCompatibleDC(dc);',
+            'Use adoptPtr and OwnPtr<HDC> when calling CreateCompatibleDC to avoid potential '
+            'memory leaks.  [runtime/leaky_pattern] [5]')
+        self.assert_leaky_pattern_check(
+            'adoptPtr(CreateCompatibleDC(dc));',
+            '')
+
+
+class WebKitStyleTest(CppStyleTestBase):
+
+    # for http://webkit.org/coding/coding-style.html
+    def test_indentation(self):
+        # 1. Use spaces, not tabs. Tabs should only appear in files that
+        #    require them for semantic meaning, like Makefiles.
+        self.assert_multi_line_lint(
+            'class Foo {\n'
+            '    int goo;\n'
+            '};',
+            '')
+        self.assert_multi_line_lint(
+            'class Foo {\n'
+            '\tint goo;\n'
+            '};',
+            'Tab found; better to use spaces  [whitespace/tab] [1]')
+
+        # 2. The indent size is 4 spaces.
+        self.assert_multi_line_lint(
+            'class Foo {\n'
+            '    int goo;\n'
+            '};',
+            '')
+        self.assert_multi_line_lint(
+            'class Foo {\n'
+            '   int goo;\n'
+            '};',
+            'Weird number of spaces at line-start.  Are you using a 4-space indent?  [whitespace/indent] [3]')
+
+        # 3. In a header, code inside a namespace should not be indented.
+        self.assert_multi_line_lint(
+            'namespace WebCore {\n\n'
+            'class Document {\n'
+            '    int myVariable;\n'
+            '};\n'
+            '}',
+            '',
+            'foo.h')
+        self.assert_multi_line_lint(
+            'namespace OuterNamespace {\n'
+            '    namespace InnerNamespace {\n'
+            '    class Document {\n'
+            '};\n'
+            '};\n'
+            '}',
+            'Code inside a namespace should not be indented.  [whitespace/indent] [4]',
+            'foo.h')
+        self.assert_multi_line_lint(
+            'namespace OuterNamespace {\n'
+            '    class Document {\n'
+            '    namespace InnerNamespace {\n'
+            '};\n'
+            '};\n'
+            '}',
+            'Code inside a namespace should not be indented.  [whitespace/indent] [4]',
+            'foo.h')
+        self.assert_multi_line_lint(
+            'namespace WebCore {\n'
+            '#if 0\n'
+            '    class Document {\n'
+            '};\n'
+            '#endif\n'
+            '}',
+            'Code inside a namespace should not be indented.  [whitespace/indent] [4]',
+            'foo.h')
+        self.assert_multi_line_lint(
+            'namespace WebCore {\n'
+            'class Document {\n'
+            '};\n'
+            '}',
+            '',
+            'foo.h')
+
+        # 4. In an implementation file (files with the extension .cpp, .c
+        #    or .mm), code inside a namespace should not be indented.
+        self.assert_multi_line_lint(
+            'namespace WebCore {\n\n'
+            'Document::Foo()\n'
+            '    : foo(bar)\n'
+            '    , boo(far)\n'
+            '{\n'
+            '    stuff();\n'
+            '}',
+            '',
+            'foo.cpp')
+        self.assert_multi_line_lint(
+            'namespace OuterNamespace {\n'
+            'namespace InnerNamespace {\n'
+            'Document::Foo() { }\n'
+            '    void* p;\n'
+            '}\n'
+            '}\n',
+            'Code inside a namespace should not be indented.  [whitespace/indent] [4]',
+            'foo.cpp')
+        self.assert_multi_line_lint(
+            'namespace OuterNamespace {\n'
+            'namespace InnerNamespace {\n'
+            'Document::Foo() { }\n'
+            '}\n'
+            '    void* p;\n'
+            '}\n',
+            'Code inside a namespace should not be indented.  [whitespace/indent] [4]',
+            'foo.cpp')
+        self.assert_multi_line_lint(
+            'namespace WebCore {\n\n'
+            '    const char* foo = "start:;"\n'
+            '        "dfsfsfs";\n'
+            '}\n',
+            'Code inside a namespace should not be indented.  [whitespace/indent] [4]',
+            'foo.cpp')
+        self.assert_multi_line_lint(
+            'namespace WebCore {\n\n'
+            'const char* foo(void* a = ";", // ;\n'
+            '    void* b);\n'
+            '    void* p;\n'
+            '}\n',
+            'Code inside a namespace should not be indented.  [whitespace/indent] [4]',
+            'foo.cpp')
+        self.assert_multi_line_lint(
+            'namespace WebCore {\n\n'
+            'const char* foo[] = {\n'
+            '    "void* b);", // ;\n'
+            '    "asfdf",\n'
+            '    }\n'
+            '    void* p;\n'
+            '}\n',
+            'Code inside a namespace should not be indented.  [whitespace/indent] [4]',
+            'foo.cpp')
+        self.assert_multi_line_lint(
+            'namespace WebCore {\n\n'
+            'const char* foo[] = {\n'
+            '    "void* b);", // }\n'
+            '    "asfdf",\n'
+            '    }\n'
+            '}\n',
+            '',
+            'foo.cpp')
+        self.assert_multi_line_lint(
+            '    namespace WebCore {\n\n'
+            '    void Document::Foo()\n'
+            '    {\n'
+            'start: // infinite loops are fun!\n'
+            '        goto start;\n'
+            '    }',
+            'namespace should never be indented.  [whitespace/indent] [4]',
+            'foo.cpp')
+        self.assert_multi_line_lint(
+            'namespace WebCore {\n'
+            '    Document::Foo() { }\n'
+            '}',
+            'Code inside a namespace should not be indented.'
+            '  [whitespace/indent] [4]',
+            'foo.cpp')
+        self.assert_multi_line_lint(
+            'namespace WebCore {\n'
+            '#define abc(x) x; \\\n'
+            '    x\n'
+            '}',
+            '',
+            'foo.cpp')
+        self.assert_multi_line_lint(
+            'namespace WebCore {\n'
+            '#define abc(x) x; \\\n'
+            '    x\n'
+            '    void* x;'
+            '}',
+            'Code inside a namespace should not be indented.  [whitespace/indent] [4]',
+            'foo.cpp')
+
+        # 5. A case label should line up with its switch statement. The
+        #    case statement is indented.
+        self.assert_multi_line_lint(
+            '    switch (condition) {\n'
+            '    case fooCondition:\n'
+            '    case barCondition:\n'
+            '        i++;\n'
+            '        break;\n'
+            '    default:\n'
+            '        i--;\n'
+            '    }\n',
+            '')
+        self.assert_multi_line_lint(
+            '    switch (condition) {\n'
+            '    case fooCondition:\n'
+            '        switch (otherCondition) {\n'
+            '        default:\n'
+            '            return;\n'
+            '        }\n'
+            '    default:\n'
+            '        i--;\n'
+            '    }\n',
+            '')
+        self.assert_multi_line_lint(
+            '    switch (condition) {\n'
+            '    case fooCondition: break;\n'
+            '    default: return;\n'
+            '    }\n',
+            '')
+        self.assert_multi_line_lint(
+            '    switch (condition) {\n'
+            '        case fooCondition:\n'
+            '        case barCondition:\n'
+            '            i++;\n'
+            '            break;\n'
+            '        default:\n'
+            '            i--;\n'
+            '    }\n',
+            'A case label should not be indented, but line up with its switch statement.'
+            '  [whitespace/indent] [4]')
+        self.assert_multi_line_lint(
+            '    switch (condition) {\n'
+            '        case fooCondition:\n'
+            '            break;\n'
+            '    default:\n'
+            '            i--;\n'
+            '    }\n',
+            'A case label should not be indented, but line up with its switch statement.'
+            '  [whitespace/indent] [4]')
+        self.assert_multi_line_lint(
+            '    switch (condition) {\n'
+            '    case fooCondition:\n'
+            '    case barCondition:\n'
+            '        switch (otherCondition) {\n'
+            '            default:\n'
+            '            return;\n'
+            '        }\n'
+            '    default:\n'
+            '        i--;\n'
+            '    }\n',
+            'A case label should not be indented, but line up with its switch statement.'
+            '  [whitespace/indent] [4]')
+        self.assert_multi_line_lint(
+            '    switch (condition) {\n'
+            '    case fooCondition:\n'
+            '    case barCondition:\n'
+            '    i++;\n'
+            '    break;\n\n'
+            '    default:\n'
+            '    i--;\n'
+            '    }\n',
+            'Non-label code inside switch statements should be indented.'
+            '  [whitespace/indent] [4]')
+        self.assert_multi_line_lint(
+            '    switch (condition) {\n'
+            '    case fooCondition:\n'
+            '    case barCondition:\n'
+            '        switch (otherCondition) {\n'
+            '        default:\n'
+            '        return;\n'
+            '        }\n'
+            '    default:\n'
+            '        i--;\n'
+            '    }\n',
+            'Non-label code inside switch statements should be indented.'
+            '  [whitespace/indent] [4]')
+
+        # 6. Boolean expressions at the same nesting level that span
+        #   multiple lines should have their operators on the left side of
+        #   the line instead of the right side.
+        self.assert_multi_line_lint(
+            '    return attr->name() == srcAttr\n'
+            '        || attr->name() == lowsrcAttr;\n',
+            '')
+        self.assert_multi_line_lint(
+            '    return attr->name() == srcAttr ||\n'
+            '        attr->name() == lowsrcAttr;\n',
+            'Boolean expressions that span multiple lines should have their '
+            'operators on the left side of the line instead of the right side.'
+            '  [whitespace/operators] [4]')
+
+    def test_spacing(self):
+        # 1. Do not place spaces around unary operators.
+        self.assert_multi_line_lint(
+            'i++;',
+            '')
+        self.assert_multi_line_lint(
+            'i ++;',
+            'Extra space for operator  ++;  [whitespace/operators] [4]')
+
+        # 2. Do place spaces around binary and ternary operators.
+        self.assert_multi_line_lint(
+            'y = m * x + b;',
+            '')
+        self.assert_multi_line_lint(
+            'f(a, b);',
+            '')
+        self.assert_multi_line_lint(
+            'c = a | b;',
+            '')
+        self.assert_multi_line_lint(
+            'return condition ? 1 : 0;',
+            '')
+        self.assert_multi_line_lint(
+            'y=m*x+b;',
+            'Missing spaces around =  [whitespace/operators] [4]')
+        self.assert_multi_line_lint(
+            'f(a,b);',
+            'Missing space after ,  [whitespace/comma] [3]')
+        self.assert_multi_line_lint(
+            'c = a|b;',
+            'Missing spaces around |  [whitespace/operators] [3]')
+        # FIXME: We cannot catch this lint error.
+        # self.assert_multi_line_lint(
+        #     'return condition ? 1:0;',
+        #     '')
+
+        # 3. Place spaces between control statements and their parentheses.
+        self.assert_multi_line_lint(
+            '    if (condition)\n'
+            '        doIt();\n',
+            '')
+        self.assert_multi_line_lint(
+            '    if(condition)\n'
+            '        doIt();\n',
+            'Missing space before ( in if(  [whitespace/parens] [5]')
+
+        # 4. Do not place spaces between a function and its parentheses,
+        #    or between a parenthesis and its content.
+        self.assert_multi_line_lint(
+            'f(a, b);',
+            '')
+        self.assert_multi_line_lint(
+            'f (a, b);',
+            'Extra space before ( in function call  [whitespace/parens] [4]')
+        self.assert_multi_line_lint(
+            'f( a, b );',
+            ['Extra space after ( in function call  [whitespace/parens] [4]',
+             'Extra space before )  [whitespace/parens] [2]'])
+
+    def test_line_breaking(self):
+        # 1. Each statement should get its own line.
+        self.assert_multi_line_lint(
+            '    x++;\n'
+            '    y++;\n'
+            '    if (condition);\n'
+            '        doIt();\n',
+            '')
+        self.assert_multi_line_lint(
+            '    if (condition) \\\n'
+            '        doIt();\n',
+            '')
+        self.assert_multi_line_lint(
+            '    x++; y++;',
+            'More than one command on the same line  [whitespace/newline] [4]')
+        self.assert_multi_line_lint(
+            '    if (condition) doIt();\n',
+            'More than one command on the same line in if  [whitespace/parens] [4]')
+        # Ensure that having a # in the line doesn't hide the error.
+        self.assert_multi_line_lint(
+            '    x++; char a[] = "#";',
+            'More than one command on the same line  [whitespace/newline] [4]')
+        # Ignore preprocessor if's.
+        self.assert_multi_line_lint(
+            '#if (condition) || (condition2)\n',
+            '')
+
+        # 2. An else statement should go on the same line as a preceding
+        #   close brace if one is present, else it should line up with the
+        #   if statement.
+        self.assert_multi_line_lint(
+            'if (condition) {\n'
+            '    doSomething();\n'
+            '    doSomethingAgain();\n'
+            '} else {\n'
+            '    doSomethingElse();\n'
+            '    doSomethingElseAgain();\n'
+            '}\n',
+            '')
+        self.assert_multi_line_lint(
+            'if (condition)\n'
+            '    doSomething();\n'
+            'else\n'
+            '    doSomethingElse();\n',
+            '')
+        self.assert_multi_line_lint(
+            'if (condition)\n'
+            '    doSomething();\n'
+            'else {\n'
+            '    doSomethingElse();\n'
+            '    doSomethingElseAgain();\n'
+            '}\n',
+            '')
+        self.assert_multi_line_lint(
+            '#define TEST_ASSERT(expression) do { if (!(expression)) { TestsController::shared().testFailed(__FILE__, __LINE__, #expression); return; } } while (0)\n',
+            '')
+        self.assert_multi_line_lint(
+            '#define TEST_ASSERT(expression) do { if ( !(expression)) { TestsController::shared().testFailed(__FILE__, __LINE__, #expression); return; } } while (0)\n',
+            'Extra space after ( in if  [whitespace/parens] [5]')
+        # FIXME: currently we only check first conditional, so we cannot detect errors in next ones.
+        # self.assert_multi_line_lint(
+        #     '#define TEST_ASSERT(expression) do { if (!(expression)) { TestsController::shared().testFailed(__FILE__, __LINE__, #expression); return; } } while (0 )\n',
+        #     'Mismatching spaces inside () in if  [whitespace/parens] [5]')
+        self.assert_multi_line_lint(
+            'WTF_MAKE_NONCOPYABLE(ClassName); WTF_MAKE_FAST_ALLOCATED;\n',
+            '')
+        self.assert_multi_line_lint(
+            'if (condition) {\n'
+            '    doSomething();\n'
+            '    doSomethingAgain();\n'
+            '}\n'
+            'else {\n'
+            '    doSomethingElse();\n'
+            '    doSomethingElseAgain();\n'
+            '}\n',
+            'An else should appear on the same line as the preceding }  [whitespace/newline] [4]')
+        self.assert_multi_line_lint(
+            'if (condition) doSomething(); else doSomethingElse();\n',
+            ['More than one command on the same line  [whitespace/newline] [4]',
+             'Else clause should never be on same line as else (use 2 lines)  [whitespace/newline] [4]',
+             'More than one command on the same line in if  [whitespace/parens] [4]'])
+        self.assert_multi_line_lint(
+            'if (condition) doSomething(); else {\n'
+            '    doSomethingElse();\n'
+            '}\n',
+            ['More than one command on the same line in if  [whitespace/parens] [4]',
+             'One line control clauses should not use braces.  [whitespace/braces] [4]'])
+        self.assert_multi_line_lint(
+            'if (condition)\n'
+            '    doSomething();\n'
+            'else {\n'
+            '    doSomethingElse();\n'
+            '}\n',
+            'One line control clauses should not use braces.  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            'if (condition) {\n'
+            '    doSomething1();\n'
+            '    doSomething2();\n'
+            '} else {\n'
+            '    doSomethingElse();\n'
+            '}\n',
+            'One line control clauses should not use braces.  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            'void func()\n'
+            '{\n'
+            '    while (condition) { }\n'
+            '    return 0;\n'
+            '}\n',
+            '')
+        self.assert_multi_line_lint(
+            'void func()\n'
+            '{\n'
+            '    for (i = 0; i < 42; i++) { foobar(); }\n'
+            '    return 0;\n'
+            '}\n',
+            'More than one command on the same line in for  [whitespace/parens] [4]')
+
+        # 3. An else if statement should be written as an if statement
+        #    when the prior if concludes with a return statement.
+        self.assert_multi_line_lint(
+            'if (motivated) {\n'
+            '    if (liquid)\n'
+            '        return money;\n'
+            '} else if (tired)\n'
+            '    break;\n',
+            '')
+        self.assert_multi_line_lint(
+            'if (condition)\n'
+            '    doSomething();\n'
+            'else if (otherCondition)\n'
+            '    doSomethingElse();\n',
+            '')
+        self.assert_multi_line_lint(
+            'if (condition)\n'
+            '    doSomething();\n'
+            'else\n'
+            '    doSomethingElse();\n',
+            '')
+        self.assert_multi_line_lint(
+            'if (condition)\n'
+            '    returnValue = foo;\n'
+            'else if (otherCondition)\n'
+            '    returnValue = bar;\n',
+            '')
+        self.assert_multi_line_lint(
+            'if (condition)\n'
+            '    returnValue = foo;\n'
+            'else\n'
+            '    returnValue = bar;\n',
+            '')
+        self.assert_multi_line_lint(
+            'if (condition)\n'
+            '    doSomething();\n'
+            'else if (liquid)\n'
+            '    return money;\n'
+            'else if (broke)\n'
+            '    return favor;\n'
+            'else\n'
+            '    sleep(28800);\n',
+            '')
+        self.assert_multi_line_lint(
+            'if (liquid) {\n'
+            '    prepare();\n'
+            '    return money;\n'
+            '} else if (greedy) {\n'
+            '    keep();\n'
+            '    return nothing;\n'
+            '}\n',
+            'An else if statement should be written as an if statement when the '
+            'prior "if" concludes with a return, break, continue or goto statement.'
+            '  [readability/control_flow] [4]')
+        self.assert_multi_line_lint(
+            '    if (stupid) {\n'
+            'infiniteLoop:\n'
+            '        goto infiniteLoop;\n'
+            '    } else if (evil)\n'
+            '        goto hell;\n',
+            'An else if statement should be written as an if statement when the '
+            'prior "if" concludes with a return, break, continue or goto statement.'
+            '  [readability/control_flow] [4]')
+        self.assert_multi_line_lint(
+            'if (liquid)\n'
+            '{\n'
+            '    prepare();\n'
+            '    return money;\n'
+            '}\n'
+            'else if (greedy)\n'
+            '    keep();\n',
+            ['This { should be at the end of the previous line  [whitespace/braces] [4]',
+            'An else should appear on the same line as the preceding }  [whitespace/newline] [4]',
+            'An else if statement should be written as an if statement when the '
+            'prior "if" concludes with a return, break, continue or goto statement.'
+            '  [readability/control_flow] [4]'])
+        self.assert_multi_line_lint(
+            'if (gone)\n'
+            '    return;\n'
+            'else if (here)\n'
+            '    go();\n',
+            'An else if statement should be written as an if statement when the '
+            'prior "if" concludes with a return, break, continue or goto statement.'
+            '  [readability/control_flow] [4]')
+        self.assert_multi_line_lint(
+            'if (gone)\n'
+            '    return;\n'
+            'else\n'
+            '    go();\n',
+            'An else statement can be removed when the prior "if" concludes '
+            'with a return, break, continue or goto statement.'
+            '  [readability/control_flow] [4]')
+        self.assert_multi_line_lint(
+            'if (motivated) {\n'
+            '    prepare();\n'
+            '    continue;\n'
+            '} else {\n'
+            '    cleanUp();\n'
+            '    break;\n'
+            '}\n',
+            'An else statement can be removed when the prior "if" concludes '
+            'with a return, break, continue or goto statement.'
+            '  [readability/control_flow] [4]')
+        self.assert_multi_line_lint(
+            'if (tired)\n'
+            '    break;\n'
+            'else {\n'
+            '    prepare();\n'
+            '    continue;\n'
+            '}\n',
+            'An else statement can be removed when the prior "if" concludes '
+            'with a return, break, continue or goto statement.'
+            '  [readability/control_flow] [4]')
+
+    def test_braces(self):
+        # 1. Function definitions: place each brace on its own line.
+        self.assert_multi_line_lint(
+            'int main()\n'
+            '{\n'
+            '    doSomething();\n'
+            '}\n',
+            '')
+        self.assert_multi_line_lint(
+            'int main() {\n'
+            '    doSomething();\n'
+            '}\n',
+            'Place brace on its own line for function definitions.  [whitespace/braces] [4]')
+
+        # 2. Other braces: place the open brace on the line preceding the
+        #    code block; place the close brace on its own line.
+        self.assert_multi_line_lint(
+            'class MyClass {\n'
+            '    int foo;\n'
+            '};\n',
+            '')
+        self.assert_multi_line_lint(
+            'namespace WebCore {\n'
+            'int foo;\n'
+            '};\n',
+            '')
+        self.assert_multi_line_lint(
+            'for (int i = 0; i < 10; i++) {\n'
+            '    DoSomething();\n'
+            '};\n',
+            '')
+        self.assert_multi_line_lint(
+            'class MyClass\n'
+            '{\n'
+            '    int foo;\n'
+            '};\n',
+            'This { should be at the end of the previous line  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            'if (condition)\n'
+            '{\n'
+            '    int foo;\n'
+            '}\n',
+            'This { should be at the end of the previous line  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            'for (int i = 0; i < 10; i++)\n'
+            '{\n'
+            '    int foo;\n'
+            '}\n',
+            'This { should be at the end of the previous line  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            'while (true)\n'
+            '{\n'
+            '    int foo;\n'
+            '}\n',
+            'This { should be at the end of the previous line  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            'foreach (Foo* foo, foos)\n'
+            '{\n'
+            '    int bar;\n'
+            '}\n',
+            'This { should be at the end of the previous line  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            'switch (type)\n'
+            '{\n'
+            'case foo: return;\n'
+            '}\n',
+            'This { should be at the end of the previous line  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            'if (condition)\n'
+            '{\n'
+            '    int foo;\n'
+            '}\n',
+            'This { should be at the end of the previous line  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            'for (int i = 0; i < 10; i++)\n'
+            '{\n'
+            '    int foo;\n'
+            '}\n',
+            'This { should be at the end of the previous line  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            'while (true)\n'
+            '{\n'
+            '    int foo;\n'
+            '}\n',
+            'This { should be at the end of the previous line  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            'switch (type)\n'
+            '{\n'
+            'case foo: return;\n'
+            '}\n',
+            'This { should be at the end of the previous line  [whitespace/braces] [4]')
+        self.assert_multi_line_lint(
+            'else if (type)\n'
+            '{\n'
+            'case foo: return;\n'
+            '}\n',
+            'This { should be at the end of the previous line  [whitespace/braces] [4]')
+
+        # 3. One-line control clauses should not use braces unless
+        #    comments are included or a single statement spans multiple
+        #    lines.
+        self.assert_multi_line_lint(
+            'if (true) {\n'
+            '    int foo;\n'
+            '}\n',
+            'One line control clauses should not use braces.  [whitespace/braces] [4]')
+
+        self.assert_multi_line_lint(
+            'for (; foo; bar) {\n'
+            '    int foo;\n'
+            '}\n',
+            'One line control clauses should not use braces.  [whitespace/braces] [4]')
+
+        self.assert_multi_line_lint(
+            'foreach (foo, foos) {\n'
+            '    int bar;\n'
+            '}\n',
+            'One line control clauses should not use braces.  [whitespace/braces] [4]')
+
+        self.assert_multi_line_lint(
+            'while (true) {\n'
+            '    int foo;\n'
+            '}\n',
+            'One line control clauses should not use braces.  [whitespace/braces] [4]')
+
+        self.assert_multi_line_lint(
+            'if (true)\n'
+            '    int foo;\n'
+            'else {\n'
+            '    int foo;\n'
+            '}\n',
+            'One line control clauses should not use braces.  [whitespace/braces] [4]')
+
+        self.assert_multi_line_lint(
+            'if (true) {\n'
+            '    int foo;\n'
+            '} else\n'
+            '    int foo;\n',
+            'One line control clauses should not use braces.  [whitespace/braces] [4]')
+
+        self.assert_multi_line_lint(
+            'if (true) {\n'
+            '    // Some comment\n'
+            '    int foo;\n'
+            '}\n',
+            '')
+
+        self.assert_multi_line_lint(
+            'if (true) {\n'
+            '    myFunction(reallyLongParam1, reallyLongParam2,\n'
+            '               reallyLongParam3);\n'
+            '}\n',
+            'Weird number of spaces at line-start.  Are you using a 4-space indent?  [whitespace/indent] [3]')
+
+        self.assert_multi_line_lint(
+            'if (true) {\n'
+            '    myFunction(reallyLongParam1, reallyLongParam2,\n'
+            '            reallyLongParam3);\n'
+            '}\n',
+            'When wrapping a line, only indent 4 spaces.  [whitespace/indent] [3]')
+
+        # 4. Control clauses without a body should use empty braces.
+        self.assert_multi_line_lint(
+            'for ( ; current; current = current->next) { }\n',
+            '')
+        self.assert_multi_line_lint(
+            'for ( ; current;\n'
+            '     current = current->next) { }\n',
+            'Weird number of spaces at line-start.  Are you using a 4-space indent?  [whitespace/indent] [3]')
+        self.assert_multi_line_lint(
+            'for ( ; current; current = current->next);\n',
+            'Semicolon defining empty statement for this loop. Use { } instead.  [whitespace/semicolon] [5]')
+        self.assert_multi_line_lint(
+            'while (true);\n',
+            'Semicolon defining empty statement for this loop. Use { } instead.  [whitespace/semicolon] [5]')
+        self.assert_multi_line_lint(
+            '} while (true);\n',
+            '')
+
+    def test_null_false_zero(self):
+        # 1. In C++, the null pointer value should be written as 0. In C,
+        #    it should be written as NULL. In Objective-C and Objective-C++,
+        #    follow the guideline for C or C++, respectively, but use nil to
+        #    represent a null Objective-C object.
+        self.assert_lint(
+            'functionCall(NULL)',
+            'Use 0 instead of NULL.'
+            '  [readability/null] [5]',
+            'foo.cpp')
+        self.assert_lint(
+            "// Don't use NULL in comments since it isn't in code.",
+            'Use 0 or null instead of NULL (even in *comments*).'
+            '  [readability/null] [4]',
+            'foo.cpp')
+        self.assert_lint(
+            '"A string with NULL" // and a comment with NULL is tricky to flag correctly in cpp_style.',
+            'Use 0 or null instead of NULL (even in *comments*).'
+            '  [readability/null] [4]',
+            'foo.cpp')
+        self.assert_lint(
+            '"A string containing NULL is ok"',
+            '',
+            'foo.cpp')
+        self.assert_lint(
+            'if (aboutNULL)',
+            '',
+            'foo.cpp')
+        self.assert_lint(
+            'myVariable = NULLify',
+            '',
+            'foo.cpp')
+        # Make sure that the NULL check does not apply to C and Objective-C files.
+        self.assert_lint(
+            'functionCall(NULL)',
+            '',
+            'foo.c')
+        self.assert_lint(
+            'functionCall(NULL)',
+            '',
+            'foo.m')
+
+        # Make sure that the NULL check does not apply to g_object_{set,get} and
+        # g_str{join,concat}
+        self.assert_lint(
+            'g_object_get(foo, "prop", &bar, NULL);',
+            '')
+        self.assert_lint(
+            'g_object_set(foo, "prop", bar, NULL);',
+            '')
+        self.assert_lint(
+            'g_build_filename(foo, bar, NULL);',
+            '')
+        self.assert_lint(
+            'gst_bin_add_many(foo, bar, boo, NULL);',
+            '')
+        self.assert_lint(
+            'gst_bin_remove_many(foo, bar, boo, NULL);',
+            '')
+        self.assert_lint(
+            'gst_element_link_many(foo, bar, boo, NULL);',
+            '')
+        self.assert_lint(
+            'gst_element_unlink_many(foo, bar, boo, NULL);',
+            '')
+        self.assert_lint(
+            'gst_structure_get(foo, "value", G_TYPE_INT, &value, NULL);',
+            '')
+        self.assert_lint(
+            'gst_structure_set(foo, "value", G_TYPE_INT, value, NULL);',
+            '')
+        self.assert_lint(
+            'gst_structure_remove_fields(foo, "value", "bar", NULL);',
+            '')
+        self.assert_lint(
+            'gst_structure_new("foo", "value", G_TYPE_INT, value, NULL);',
+            '')
+        self.assert_lint(
+            'gst_structure_id_new(FOO, VALUE, G_TYPE_INT, value, NULL);',
+            '')
+        self.assert_lint(
+            'gst_structure_id_set(FOO, VALUE, G_TYPE_INT, value, NULL);',
+            '')
+        self.assert_lint(
+            'gst_structure_id_get(FOO, VALUE, G_TYPE_INT, &value, NULL);',
+            '')
+        self.assert_lint(
+            'gst_caps_new_simple(mime, "value", G_TYPE_INT, &value, NULL);',
+            '')
+        self.assert_lint(
+            'gst_caps_new_full(structure1, structure2, NULL);',
+            '')
+        self.assert_lint(
+            'gchar* result = g_strconcat("part1", "part2", "part3", NULL);',
+            '')
+        self.assert_lint(
+            'gchar* result = g_strconcat("part1", NULL);',
+            '')
+        self.assert_lint(
+            'gchar* result = g_strjoin(",", "part1", "part2", "part3", NULL);',
+            '')
+        self.assert_lint(
+            'gchar* result = g_strjoin(",", "part1", NULL);',
+            '')
+        self.assert_lint(
+            'gchar* result = gdk_pixbuf_save_to_callback(pixbuf, function, data, type, error, NULL);',
+            '')
+        self.assert_lint(
+            'gchar* result = gdk_pixbuf_save_to_buffer(pixbuf, function, data, type, error, NULL);',
+            '')
+        self.assert_lint(
+            'gchar* result = gdk_pixbuf_save_to_stream(pixbuf, function, data, type, error, NULL);',
+            '')
+        self.assert_lint(
+            'gtk_widget_style_get(style, "propertyName", &value, "otherName", &otherValue, NULL);',
+            '')
+        self.assert_lint(
+            'gtk_style_context_get_style(context, "propertyName", &value, "otherName", &otherValue, NULL);',
+            '')
+        self.assert_lint(
+            'gtk_widget_style_get_property(style, NULL, NULL);',
+            'Use 0 instead of NULL.  [readability/null] [5]',
+            'foo.cpp')
+        self.assert_lint(
+            'gtk_widget_style_get_valist(style, NULL, NULL);',
+            'Use 0 instead of NULL.  [readability/null] [5]',
+            'foo.cpp')
+
+        # 2. C++ and C bool values should be written as true and
+        #    false. Objective-C BOOL values should be written as YES and NO.
+        # FIXME: Implement this.
+
+        # 3. Tests for true/false, null/non-null, and zero/non-zero should
+        #    all be done without equality comparisons.
+        self.assert_lint(
+            'if (count == 0)',
+            'Tests for true/false, null/non-null, and zero/non-zero should all be done without equality comparisons.'
+            '  [readability/comparison_to_zero] [5]')
+        self.assert_lint_one_of_many_errors_re(
+            'if (string != NULL)',
+            r'Tests for true/false, null/non-null, and zero/non-zero should all be done without equality comparisons\.')
+        self.assert_lint(
+            'if (condition == true)',
+            'Tests for true/false, null/non-null, and zero/non-zero should all be done without equality comparisons.'
+            '  [readability/comparison_to_zero] [5]')
+        self.assert_lint(
+            'if (myVariable != /* Why would anyone put a comment here? */ false)',
+            'Tests for true/false, null/non-null, and zero/non-zero should all be done without equality comparisons.'
+            '  [readability/comparison_to_zero] [5]')
+
+        self.assert_lint(
+            'if (0 /* This comment also looks odd to me. */ != aLongerVariableName)',
+            'Tests for true/false, null/non-null, and zero/non-zero should all be done without equality comparisons.'
+            '  [readability/comparison_to_zero] [5]')
+        self.assert_lint_one_of_many_errors_re(
+            'if (NULL == thisMayBeNull)',
+            r'Tests for true/false, null/non-null, and zero/non-zero should all be done without equality comparisons\.')
+        self.assert_lint(
+            'if (true != anotherCondition)',
+            'Tests for true/false, null/non-null, and zero/non-zero should all be done without equality comparisons.'
+            '  [readability/comparison_to_zero] [5]')
+        self.assert_lint(
+            'if (false == myBoolValue)',
+            'Tests for true/false, null/non-null, and zero/non-zero should all be done without equality comparisons.'
+            '  [readability/comparison_to_zero] [5]')
+
+        self.assert_lint(
+            'if (fontType == trueType)',
+            '')
+        self.assert_lint(
+            'if (othertrue == fontType)',
+            '')
+        self.assert_lint(
+            'if (LIKELY(foo == 0))',
+            '')
+        self.assert_lint(
+            'if (UNLIKELY(foo == 0))',
+            '')
+        self.assert_lint(
+            'if ((a - b) == 0.5)',
+            '')
+        self.assert_lint(
+            'if (0.5 == (a - b))',
+            '')
+        self.assert_lint(
+            'if (LIKELY(foo == NULL))',
+            'Use 0 instead of NULL.  [readability/null] [5]')
+        self.assert_lint(
+            'if (UNLIKELY(foo == NULL))',
+            'Use 0 instead of NULL.  [readability/null] [5]')
+
+    def test_directive_indentation(self):
+        self.assert_lint(
+            "    #if FOO",
+            "preprocessor directives (e.g., #ifdef, #define, #import) should never be indented."
+            "  [whitespace/indent] [4]",
+            "foo.cpp")
+
+    def test_using_std(self):
+        self.assert_lint(
+            'using std::min;',
+            "Use 'using namespace std;' instead of 'using std::min;'."
+            "  [build/using_std] [4]",
+            'foo.cpp')
+
+    def test_max_macro(self):
+        self.assert_lint(
+            'int i = MAX(0, 1);',
+            '',
+            'foo.c')
+
+        self.assert_lint(
+            'int i = MAX(0, 1);',
+            'Use std::max() or std::max<type>() instead of the MAX() macro.'
+            '  [runtime/max_min_macros] [4]',
+            'foo.cpp')
+
+        self.assert_lint(
+            'inline int foo() { return MAX(0, 1); }',
+            'Use std::max() or std::max<type>() instead of the MAX() macro.'
+            '  [runtime/max_min_macros] [4]',
+            'foo.h')
+
+    def test_min_macro(self):
+        self.assert_lint(
+            'int i = MIN(0, 1);',
+            '',
+            'foo.c')
+
+        self.assert_lint(
+            'int i = MIN(0, 1);',
+            'Use std::min() or std::min<type>() instead of the MIN() macro.'
+            '  [runtime/max_min_macros] [4]',
+            'foo.cpp')
+
+        self.assert_lint(
+            'inline int foo() { return MIN(0, 1); }',
+            'Use std::min() or std::min<type>() instead of the MIN() macro.'
+            '  [runtime/max_min_macros] [4]',
+            'foo.h')
+
+    def test_ctype_fucntion(self):
+        self.assert_lint(
+            'int i = isascii(8);',
+            'Use equivelent function in <wtf/ASCIICType.h> instead of the '
+            'isascii() function.  [runtime/ctype_function] [4]',
+            'foo.cpp')
+
+    def test_names(self):
+        name_underscore_error_message = " is incorrectly named. Don't use underscores in your identifier names.  [readability/naming/underscores] [4]"
+        name_tooshort_error_message = " is incorrectly named. Don't use the single letter 'l' as an identifier name.  [readability/naming] [4]"
+
+        # Basic cases from WebKit style guide.
+        self.assert_lint('struct Data;', '')
+        self.assert_lint('size_t bufferSize;', '')
+        self.assert_lint('class HTMLDocument;', '')
+        self.assert_lint('String mimeType();', '')
+        self.assert_lint('size_t buffer_size;',
+                         'buffer_size' + name_underscore_error_message)
+        self.assert_lint('short m_length;', '')
+        self.assert_lint('short _length;',
+                         '_length' + name_underscore_error_message)
+        self.assert_lint('short length_;',
+                         'length_' + name_underscore_error_message)
+        self.assert_lint('unsigned _length;',
+                         '_length' + name_underscore_error_message)
+        self.assert_lint('unsigned long _length;',
+                         '_length' + name_underscore_error_message)
+        self.assert_lint('unsigned long long _length;',
+                         '_length' + name_underscore_error_message)
+
+        # Allow underscores in Objective C files.
+        self.assert_lint('unsigned long long _length;',
+                         '',
+                         'foo.m')
+        self.assert_lint('unsigned long long _length;',
+                         '',
+                         'foo.mm')
+        self.assert_lint('#import "header_file.h"\n'
+                         'unsigned long long _length;',
+                         '',
+                         'foo.h')
+        self.assert_lint('unsigned long long _length;\n'
+                         '@interface WebFullscreenWindow;',
+                         '',
+                         'foo.h')
+        self.assert_lint('unsigned long long _length;\n'
+                         '@implementation WebFullscreenWindow;',
+                         '',
+                         'foo.h')
+        self.assert_lint('unsigned long long _length;\n'
+                         '@class WebWindowFadeAnimation;',
+                         '',
+                         'foo.h')
+
+        # Variable name 'l' is easy to confuse with '1'
+        self.assert_lint('int l;', 'l' + name_tooshort_error_message)
+        self.assert_lint('size_t l;', 'l' + name_tooshort_error_message)
+        self.assert_lint('long long l;', 'l' + name_tooshort_error_message)
+
+        # Pointers, references, functions, templates, and adjectives.
+        self.assert_lint('char* under_score;',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('const int UNDER_SCORE;',
+                         'UNDER_SCORE' + name_underscore_error_message)
+        self.assert_lint('static inline const char const& const under_score;',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('WebCore::RenderObject* under_score;',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('int func_name();',
+                         'func_name' + name_underscore_error_message)
+        self.assert_lint('RefPtr<RenderObject*> under_score;',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('WTF::Vector<WTF::RefPtr<const RenderObject* const> > under_score;',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('int under_score[];',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('struct dirent* under_score;',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('long under_score;',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('long long under_score;',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('long double under_score;',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('long long int under_score;',
+                         'under_score' + name_underscore_error_message)
+
+        # Declarations in control statement.
+        self.assert_lint('if (int under_score = 42) {',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('else if (int under_score = 42) {',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('for (int under_score = 42; cond; i++) {',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('while (foo & under_score = bar) {',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('for (foo * under_score = p; cond; i++) {',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('for (foo * under_score; cond; i++) {',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('while (foo & value_in_thirdparty_library) {', '')
+        self.assert_lint('while (foo * value_in_thirdparty_library) {', '')
+        self.assert_lint('if (mli && S_OK == mli->foo()) {', '')
+
+        # More member variables and functions.
+        self.assert_lint('int SomeClass::s_validName', '')
+        self.assert_lint('int m_under_score;',
+                         'm_under_score' + name_underscore_error_message)
+        self.assert_lint('int SomeClass::s_under_score = 0;',
+                         'SomeClass::s_under_score' + name_underscore_error_message)
+        self.assert_lint('int SomeClass::under_score = 0;',
+                         'SomeClass::under_score' + name_underscore_error_message)
+
+        # Other statements.
+        self.assert_lint('return INT_MAX;', '')
+        self.assert_lint('return_t under_score;',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('goto under_score;',
+                         'under_score' + name_underscore_error_message)
+        self.assert_lint('delete static_cast<Foo*>(p);', '')
+
+        # Multiple variables in one line.
+        self.assert_lint('void myFunction(int variable1, int another_variable);',
+                         'another_variable' + name_underscore_error_message)
+        self.assert_lint('int variable1, another_variable;',
+                         'another_variable' + name_underscore_error_message)
+        self.assert_lint('int first_variable, secondVariable;',
+                         'first_variable' + name_underscore_error_message)
+        self.assert_lint('void my_function(int variable_1, int variable_2);',
+                         ['my_function' + name_underscore_error_message,
+                          'variable_1' + name_underscore_error_message,
+                          'variable_2' + name_underscore_error_message])
+        self.assert_lint('for (int variable_1, variable_2;;) {',
+                         ['variable_1' + name_underscore_error_message,
+                          'variable_2' + name_underscore_error_message])
+
+        # 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)', '', 'Source/JavaScriptCore/foo.cpp')
+        self.assert_lint('void op_code(int var1, int var2)', '', 'Source/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.
+        self.assert_lint('void webkit_dom_object_init();', '')
+        self.assert_lint('void webkit_dom_object_class_init();', '')
+
+        # There is an exception for GTK+ API.
+        self.assert_lint('void webkit_web_view_load(int var1, int var2)', '', 'Source/Webkit/gtk/webkit/foo.cpp')
+        self.assert_lint('void webkit_web_view_load(int var1, int var2)', '', 'Source/Webkit2/UIProcess/gtk/foo.cpp')
+
+        # Test that this doesn't also apply to files not in a 'gtk' directory.
+        self.assert_lint('void webkit_web_view_load(int var1, int var2)',
+            'webkit_web_view_load is incorrectly named. Don\'t use underscores in your identifier names.'
+            '  [readability/naming/underscores] [4]', 'Source/Webkit/webkit/foo.cpp')
+        # Test that this doesn't also apply to names that don't start with 'webkit_'.
+        self.assert_lint_one_of_many_errors_re('void otherkit_web_view_load(int var1, int var2)',
+            'otherkit_web_view_load is incorrectly named. Don\'t use underscores in your identifier names.'
+            '  [readability/naming/underscores] [4]', 'Source/Webkit/webkit/foo.cpp')
+
+        # There is an exception for some unit tests that begin with "tst_".
+        self.assert_lint('void tst_QWebFrame::arrayObjectEnumerable(int var1, int var2)', '')
+
+        # The Qt API uses names that begin with "qt_" or "_q_".
+        self.assert_lint('void QTFrame::qt_drt_is_awesome(int var1, int var2)', '')
+        self.assert_lint('void QTFrame::_q_drt_is_awesome(int var1, int var2)', '')
+        self.assert_lint('void qt_drt_is_awesome(int var1, int var2);', '')
+        self.assert_lint('void _q_drt_is_awesome(int var1, int var2);', '')
+
+        # Cairo forward-declarations should not be a failure.
+        self.assert_lint('typedef struct _cairo cairo_t;', '')
+        self.assert_lint('typedef struct _cairo_surface cairo_surface_t;', '')
+        self.assert_lint('typedef struct _cairo_scaled_font cairo_scaled_font_t;', '')
+
+        # EFL forward-declarations should not be a failure.
+        self.assert_lint('typedef struct _Ecore_Evas Ecore_Evas;', '')
+        self.assert_lint('typedef struct _Ecore_Pipe Ecore_Pipe;', '')
+        self.assert_lint('typedef struct _Eina_Rectangle Eina_Rectangle;', '')
+        self.assert_lint('typedef struct _Evas_Object Evas_Object;', '')
+        self.assert_lint('typedef struct _Ewk_History_Item Ewk_History_Item;', '')
+
+        # NPAPI functions that start with NPN_, NPP_ or NP_ are allowed.
+        self.assert_lint('void NPN_Status(NPP, const char*)', '')
+        self.assert_lint('NPError NPP_SetWindow(NPP instance, NPWindow *window)', '')
+        self.assert_lint('NPObject* NP_Allocate(NPP, NPClass*)', '')
+
+        # 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)
+
+        # new operators in initialization.
+        self.assert_lint('OwnPtr<uint32_t> variable(new uint32_t);', '')
+        self.assert_lint('OwnPtr<uint32_t> variable(new (expr) uint32_t);', '')
+        self.assert_lint('OwnPtr<uint32_t> under_score(new uint32_t);',
+                         'under_score' + name_underscore_error_message)
+
+    def test_parameter_names(self):
+        # Leave meaningless variable names out of function declarations.
+        meaningless_variable_name_error_message = 'The parameter name "%s" adds no information, so it should be removed.  [readability/parameter_name] [5]'
+
+        parameter_error_rules = ('-',
+                                 '+readability/parameter_name')
+        # No variable name, so no error.
+        self.assertEquals('',
+                          self.perform_lint('void func(int);', 'test.cpp', parameter_error_rules))
+
+        # Verify that copying the name of the set function causes the error (with some odd casing).
+        self.assertEquals(meaningless_variable_name_error_message % 'itemCount',
+                          self.perform_lint('void setItemCount(size_t itemCount);', 'test.cpp', parameter_error_rules))
+        self.assertEquals(meaningless_variable_name_error_message % 'abcCount',
+                          self.perform_lint('void setABCCount(size_t abcCount);', 'test.cpp', parameter_error_rules))
+
+        # Verify that copying a type name will trigger the warning (even if the type is a template parameter).
+        self.assertEquals(meaningless_variable_name_error_message % 'context',
+                          self.perform_lint('void funct(PassRefPtr<ScriptExecutionContext> context);', 'test.cpp', parameter_error_rules))
+
+        # Verify that acronyms as variable names trigger the error (for both set functions and type names).
+        self.assertEquals(meaningless_variable_name_error_message % 'ec',
+                          self.perform_lint('void setExceptionCode(int ec);', 'test.cpp', parameter_error_rules))
+        self.assertEquals(meaningless_variable_name_error_message % 'ec',
+                          self.perform_lint('void funct(ExceptionCode ec);', 'test.cpp', parameter_error_rules))
+
+        # 'object' alone, appended, or as part of an acronym is meaningless.
+        self.assertEquals(meaningless_variable_name_error_message % 'object',
+                          self.perform_lint('void funct(RenderView object);', 'test.cpp', parameter_error_rules))
+        self.assertEquals(meaningless_variable_name_error_message % 'viewObject',
+                          self.perform_lint('void funct(RenderView viewObject);', 'test.cpp', parameter_error_rules))
+        self.assertEquals(meaningless_variable_name_error_message % 'rvo',
+                          self.perform_lint('void funct(RenderView rvo);', 'test.cpp', parameter_error_rules))
+
+        # Check that r, g, b, and a are allowed.
+        self.assertEquals('',
+                          self.perform_lint('void setRGBAValues(int r, int g, int b, int a);', 'test.cpp', parameter_error_rules))
+
+        # Verify that a simple substring match isn't done which would cause false positives.
+        self.assertEquals('',
+                          self.perform_lint('void setNateLateCount(size_t elate);', 'test.cpp', parameter_error_rules))
+        self.assertEquals('',
+                          self.perform_lint('void funct(NateLate elate);', 'test.cpp', parameter_error_rules))
+
+        # Don't have generate warnings for functions (only declarations).
+        self.assertEquals('',
+                          self.perform_lint('void funct(PassRefPtr<ScriptExecutionContext> context)\n'
+                                            '{\n'
+                                            '}\n', 'test.cpp', parameter_error_rules))
+
+    def test_comments(self):
+        # A comment at the beginning of a line is ok.
+        self.assert_lint('// comment', '')
+        self.assert_lint('    // comment', '')
+
+        self.assert_lint('}  // namespace WebCore',
+                         'One space before end of line comments'
+                         '  [whitespace/comments] [5]')
+
+    def test_webkit_export_check(self):
+        webkit_export_error_rules = ('-',
+                                  '+readability/webkit_export')
+        self.assertEquals('',
+                          self.perform_lint('WEBKIT_EXPORT int foo();\n',
+                                            'WebKit/chromium/public/test.h',
+                                            webkit_export_error_rules))
+        self.assertEquals('',
+                          self.perform_lint('WEBKIT_EXPORT int foo();\n',
+                                            'WebKit/chromium/tests/test.h',
+                                            webkit_export_error_rules))
+        self.assertEquals('WEBKIT_EXPORT should only be used in header files.  [readability/webkit_export] [5]',
+                          self.perform_lint('WEBKIT_EXPORT int foo();\n',
+                                            'WebKit/chromium/public/test.cpp',
+                                            webkit_export_error_rules))
+        self.assertEquals('WEBKIT_EXPORT should only appear in the chromium public (or tests) directory.  [readability/webkit_export] [5]',
+                          self.perform_lint('WEBKIT_EXPORT int foo();\n',
+                                            'WebKit/chromium/src/test.h',
+                                            webkit_export_error_rules))
+        self.assertEquals('WEBKIT_EXPORT should not be used on a function with a body.  [readability/webkit_export] [5]',
+                          self.perform_lint('WEBKIT_EXPORT int foo() { }\n',
+                                            'WebKit/chromium/public/test.h',
+                                            webkit_export_error_rules))
+        self.assertEquals('WEBKIT_EXPORT should not be used on a function with a body.  [readability/webkit_export] [5]',
+                          self.perform_lint('WEBKIT_EXPORT inline int foo()\n'
+                                            '{\n'
+                                            '}\n',
+                                            'WebKit/chromium/public/test.h',
+                                            webkit_export_error_rules))
+        self.assertEquals('WEBKIT_EXPORT should not be used with a pure virtual function.  [readability/webkit_export] [5]',
+                          self.perform_lint('{}\n'
+                                            'WEBKIT_EXPORT\n'
+                                            'virtual\n'
+                                            'int\n'
+                                            'foo() = 0;\n',
+                                            'WebKit/chromium/public/test.h',
+                                            webkit_export_error_rules))
+        self.assertEquals('',
+                          self.perform_lint('{}\n'
+                                            'WEBKIT_EXPORT\n'
+                                            'virtual\n'
+                                            'int\n'
+                                            'foo() = 0;\n',
+                                            'test.h',
+                                            webkit_export_error_rules))
+
+    def test_other(self):
+        # FIXME: Implement this.
+        pass
+
+
+class CppCheckerTest(unittest.TestCase):
+
+    """Tests CppChecker class."""
+
+    def mock_handle_style_error(self):
+        pass
+
+    def _checker(self):
+        return CppChecker("foo", "h", self.mock_handle_style_error, 3)
+
+    def test_init(self):
+        """Test __init__ constructor."""
+        checker = self._checker()
+        self.assertEquals(checker.file_extension, "h")
+        self.assertEquals(checker.file_path, "foo")
+        self.assertEquals(checker.handle_style_error, self.mock_handle_style_error)
+        self.assertEquals(checker.min_confidence, 3)
+
+    def test_eq(self):
+        """Test __eq__ equality function."""
+        checker1 = self._checker()
+        checker2 = self._checker()
+
+        # == calls __eq__.
+        self.assertTrue(checker1 == checker2)
+
+        def mock_handle_style_error2(self):
+            pass
+
+        # Verify that a difference in any argument cause equality to fail.
+        checker = CppChecker("foo", "h", self.mock_handle_style_error, 3)
+        self.assertFalse(checker == CppChecker("bar", "h", self.mock_handle_style_error, 3))
+        self.assertFalse(checker == CppChecker("foo", "c", self.mock_handle_style_error, 3))
+        self.assertFalse(checker == CppChecker("foo", "h", mock_handle_style_error2, 3))
+        self.assertFalse(checker == CppChecker("foo", "h", self.mock_handle_style_error, 4))
+
+    def test_ne(self):
+        """Test __ne__ inequality function."""
+        checker1 = self._checker()
+        checker2 = self._checker()
+
+        # != calls __ne__.
+        # By default, __ne__ always returns true on different objects.
+        # Thus, just check the distinguishing case to verify that the
+        # code defines __ne__.
+        self.assertFalse(checker1 != checker2)
+
+
+def tearDown():
+    """A global check to make sure all error-categories have been tested.
+
+    The main tearDown() routine is the only code we can guarantee will be
+    run after all other tests have been executed.
+    """
+    try:
+        if _run_verifyallcategoriesseen:
+            ErrorCollector(None).verify_all_categories_are_seen()
+    except NameError:
+        # If nobody set the global _run_verifyallcategoriesseen, then
+        # we assume we shouldn't run the test
+        pass
+
+if __name__ == '__main__':
+    import sys
+    # We don't want to run the verify_all_categories_are_seen() test unless
+    # we're running the full test suite: if we only run one test,
+    # obviously we're not going to see all the error categories.  So we
+    # only run verify_all_categories_are_seen() when no commandline flags
+    # are passed in.
+    global _run_verifyallcategoriesseen
+    _run_verifyallcategoriesseen = (len(sys.argv) == 1)
+
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/style/checkers/jsonchecker.py b/Tools/Scripts/webkitpy/style/checkers/jsonchecker.py
new file mode 100644
index 0000000..264cbee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/jsonchecker.py
@@ -0,0 +1,49 @@
+# Copyright (C) 2011 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.
+
+"""Checks WebKit style for JSON files."""
+
+import json
+import re
+
+
+class JSONChecker(object):
+    """Processes JSON lines for checking style."""
+
+    categories = set(('json/syntax',))
+
+    def __init__(self, file_path, handle_style_error):
+        self._handle_style_error = handle_style_error
+        self._handle_style_error.turn_off_line_filtering()
+
+    def check(self, lines):
+        try:
+            json.loads('\n'.join(lines) + '\n')
+        except ValueError, e:
+            self._handle_style_error(self.line_number_from_json_exception(e), 'json/syntax', 5, str(e))
+
+    @staticmethod
+    def line_number_from_json_exception(error):
+        match = re.search(r': line (?P<line>\d+) column \d+', str(error))
+        if not match:
+            return 0
+        return int(match.group('line'))
diff --git a/Tools/Scripts/webkitpy/style/checkers/jsonchecker_unittest.py b/Tools/Scripts/webkitpy/style/checkers/jsonchecker_unittest.py
new file mode 100755
index 0000000..973c673
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/jsonchecker_unittest.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+#
+# 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.
+
+"""Unit test for jsonchecker.py."""
+
+import unittest
+
+import jsonchecker
+
+
+class MockErrorHandler(object):
+    def __init__(self, handle_style_error):
+        self.turned_off_filtering = False
+        self._handle_style_error = handle_style_error
+
+    def turn_off_line_filtering(self):
+        self.turned_off_filtering = True
+
+    def __call__(self, line_number, category, confidence, message):
+        self._handle_style_error(self, line_number, category, confidence, message)
+        return True
+
+
+class JSONCheckerTest(unittest.TestCase):
+    """Tests JSONChecker class."""
+
+    def test_line_number_from_json_exception(self):
+        tests = (
+            (0, 'No JSON object could be decoded'),
+            (2, 'Expecting property name: line 2 column 1 (char 2)'),
+            (3, 'Expecting object: line 3 column 1 (char 15)'),
+            (9, 'Expecting property name: line 9 column 21 (char 478)'),
+        )
+        for expected_line, message in tests:
+            self.assertEqual(expected_line, jsonchecker.JSONChecker.line_number_from_json_exception(ValueError(message)))
+
+    def assert_no_error(self, json_data):
+        def handle_style_error(mock_error_handler, line_number, category, confidence, message):
+            self.fail('Unexpected error: %d %s %d %s' % (line_number, category, confidence, message))
+
+        error_handler = MockErrorHandler(handle_style_error)
+        checker = jsonchecker.JSONChecker('foo.json', error_handler)
+        checker.check(json_data.split('\n'))
+        self.assertTrue(error_handler.turned_off_filtering)
+
+    def assert_error(self, expected_line_number, expected_category, json_data):
+        def handle_style_error(mock_error_handler, line_number, category, confidence, message):
+            mock_error_handler.had_error = True
+            self.assertEquals(expected_line_number, line_number)
+            self.assertEquals(expected_category, category)
+            self.assertTrue(category in jsonchecker.JSONChecker.categories)
+
+        error_handler = MockErrorHandler(handle_style_error)
+        error_handler.had_error = False
+
+        checker = jsonchecker.JSONChecker('foo.json', error_handler)
+        checker.check(json_data.split('\n'))
+        self.assertTrue(error_handler.had_error)
+        self.assertTrue(error_handler.turned_off_filtering)
+
+    def mock_handle_style_error(self):
+        pass
+
+    def test_conflict_marker(self):
+        self.assert_error(0, 'json/syntax', '<<<<<<< HEAD\n{\n}\n')
+
+    def test_single_quote(self):
+        self.assert_error(2, 'json/syntax', "{\n'slaves': []\n}\n")
+
+    def test_init(self):
+        error_handler = MockErrorHandler(self.mock_handle_style_error)
+        checker = jsonchecker.JSONChecker('foo.json', error_handler)
+        self.assertEquals(checker._handle_style_error, error_handler)
+
+    def test_no_error(self):
+        self.assert_no_error("""{
+    "slaves":     [ { "name": "test-slave", "platform": "*" },
+                    { "name": "apple-xserve-4", "platform": "mac-snowleopard" }
+                  ],
+
+    "builders":   [ { "name": "SnowLeopard Intel Release (Build)", "type": "Build", "builddir": "snowleopard-intel-release",
+                      "platform": "mac-snowleopard", "configuration": "release", "architectures": ["x86_64"],
+                      "slavenames": ["apple-xserve-4"]
+                    }
+                   ],
+
+    "schedulers": [ { "type": "PlatformSpecificScheduler", "platform": "mac-snowleopard", "branch": "trunk", "treeStableTimer": 45.0,
+                      "builderNames": ["SnowLeopard Intel Release (Build)", "SnowLeopard Intel Debug (Build)"]
+                    }
+                  ]
+}
+""")
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/style/checkers/png.py b/Tools/Scripts/webkitpy/style/checkers/png.py
new file mode 100644
index 0000000..430d6f0
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/png.py
@@ -0,0 +1,75 @@
+# Copyright (C) 2012 Balazs Ankes (bank@inf.u-szeged.hu) University of Szeged
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#
+# 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.
+
+
+"""Supports checking WebKit style in png files."""
+
+import os
+import re
+
+from webkitpy.common import checksvnconfigfile
+from webkitpy.common import read_checksum_from_png
+from webkitpy.common.system.systemhost import SystemHost
+from webkitpy.common.checkout.scm.detection import SCMDetector
+
+class PNGChecker(object):
+    """Check svn:mime-type for checking style"""
+
+    categories = set(['image/png'])
+
+    def __init__(self, file_path, handle_style_error, scm=None, host=None):
+        self._file_path = file_path
+        self._handle_style_error = handle_style_error
+        self._host = host or SystemHost()
+        self._fs = self._host.filesystem
+        self._detector = scm or SCMDetector(self._fs, self._host.executive).detect_scm_system(self._fs.getcwd())
+
+    def check(self, inline=None):
+        errorstr = ""
+        config_file_path = ""
+        detection = self._detector.display_name()
+
+        if self._fs.exists(self._file_path) and self._file_path.endswith("-expected.png"):
+            with self._fs.open_binary_file_for_reading(self._file_path) as filehandle:
+                if not read_checksum_from_png.read_checksum(filehandle):
+                    self._handle_style_error(0, 'image/png', 5, "Image lacks a checksum. Generate pngs using run-webkit-tests to ensure they have a checksum.")
+
+        if detection == "git":
+            (file_missing, autoprop_missing, png_missing) = checksvnconfigfile.check(self._host, self._fs)
+            config_file_path = checksvnconfigfile.config_file_path(self._host, self._fs)
+
+            if file_missing:
+                self._handle_style_error(0, 'image/png', 5, "There is no SVN config file. (%s)" % config_file_path)
+            elif autoprop_missing and png_missing:
+                self._handle_style_error(0, 'image/png', 5, checksvnconfigfile.errorstr_autoprop(config_file_path) + checksvnconfigfile.errorstr_png(config_file_path))
+            elif autoprop_missing:
+                self._handle_style_error(0, 'image/png', 5, checksvnconfigfile.errorstr_autoprop(config_file_path))
+            elif png_missing:
+                self._handle_style_error(0, 'image/png', 5, checksvnconfigfile.errorstr_png(config_file_path))
+
+        elif detection == "svn":
+            prop_get = self._detector.propget("svn:mime-type", self._file_path)
+            if prop_get != "image/png":
+                errorstr = "Set the svn:mime-type property (svn propset svn:mime-type image/png %s)." % self._file_path
+                self._handle_style_error(0, 'image/png', 5, errorstr)
+
diff --git a/Tools/Scripts/webkitpy/style/checkers/png_unittest.py b/Tools/Scripts/webkitpy/style/checkers/png_unittest.py
new file mode 100644
index 0000000..764c285
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/png_unittest.py
@@ -0,0 +1,134 @@
+# Copyright (C) 2012 Balazs Ankes (bank@inf.u-szeged.hu) University of Szeged
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#
+# 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.
+
+"""Unit test for png.py."""
+
+import unittest
+from png import PNGChecker
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+
+
+class MockSCMDetector(object):
+
+    def __init__(self, scm, prop=None):
+        self._scm = scm
+        self._prop = prop
+
+    def display_name(self):
+        return self._scm
+
+    def propget(self, pname, path):
+        return self._prop
+
+
+class PNGCheckerTest(unittest.TestCase):
+    """Tests PNGChecker class."""
+
+    def test_init(self):
+        """Test __init__() method."""
+
+        def mock_handle_style_error(self):
+            pass
+
+        checker = PNGChecker("test/config", mock_handle_style_error, MockSCMDetector('git'), MockSystemHost())
+        self.assertEquals(checker._file_path, "test/config")
+        self.assertEquals(checker._handle_style_error, mock_handle_style_error)
+
+    def test_check(self):
+        errors = []
+
+        def mock_handle_style_error(line_number, category, confidence, message):
+            error = (line_number, category, confidence, message)
+            errors.append(error)
+
+        file_path = ''
+
+        fs = MockFileSystem()
+
+        scm = MockSCMDetector('svn')
+        checker = PNGChecker(file_path, mock_handle_style_error, scm, MockSystemHost(filesystem=fs))
+        checker.check()
+        self.assertEquals(len(errors), 1)
+        self.assertEquals(errors[0],
+                          (0, 'image/png', 5, 'Set the svn:mime-type property (svn propset svn:mime-type image/png ).'))
+
+        files = {'/Users/mock/.subversion/config': 'enable-auto-props = yes\n*.png = svn:mime-type=image/png'}
+        fs = MockFileSystem(files)
+        scm = MockSCMDetector('git')
+        errors = []
+        checker = PNGChecker("config", mock_handle_style_error, scm, MockSystemHost(os_name='linux', filesystem=fs))
+        checker.check()
+        self.assertEquals(len(errors), 0)
+
+        files = {'/Users/mock/.subversion/config': '#enable-auto-props = yes'}
+        fs = MockFileSystem(files)
+        scm = MockSCMDetector('git')
+        errors = []
+        checker = PNGChecker("config", mock_handle_style_error, scm, MockSystemHost(os_name='linux', filesystem=fs))
+        checker.check()
+        self.assertEquals(len(errors), 1)
+
+        files = {'/Users/mock/.subversion/config': 'enable-auto-props = yes\n#enable-auto-props = yes\n*.png = svn:mime-type=image/png'}
+        fs = MockFileSystem(files)
+        scm = MockSCMDetector('git')
+        errors = []
+        checker = PNGChecker("config", mock_handle_style_error, scm, MockSystemHost(os_name='linux', filesystem=fs))
+        checker.check()
+        self.assertEquals(len(errors), 0)
+
+        files = {'/Users/mock/.subversion/config': '#enable-auto-props = yes\nenable-auto-props = yes\n*.png = svn:mime-type=image/png'}
+        fs = MockFileSystem(files)
+        scm = MockSCMDetector('git')
+        errors = []
+        checker = PNGChecker("config", mock_handle_style_error, scm, MockSystemHost(os_name='linux', filesystem=fs))
+        checker.check()
+        self.assertEquals(len(errors), 0)
+
+        files = {'/Users/mock/.subversion/config': 'enable-auto-props = no'}
+        fs = MockFileSystem(files)
+        scm = MockSCMDetector('git')
+        errors = []
+        checker = PNGChecker("config", mock_handle_style_error, scm, MockSystemHost(os_name='linux', filesystem=fs))
+        checker.check()
+        self.assertEquals(len(errors), 1)
+
+        file_path = "foo.png"
+        fs.write_binary_file(file_path, "Dummy binary data")
+        scm = MockSCMDetector('git')
+        errors = []
+        checker = PNGChecker(file_path, mock_handle_style_error, scm, MockSystemHost(os_name='linux', filesystem=fs))
+        checker.check()
+        self.assertEquals(len(errors), 1)
+
+        file_path = "foo-expected.png"
+        fs.write_binary_file(file_path, "Dummy binary data")
+        scm = MockSCMDetector('git')
+        errors = []
+        checker = PNGChecker(file_path, mock_handle_style_error, scm, MockSystemHost(os_name='linux', filesystem=fs))
+        checker.check()
+        self.assertEquals(len(errors), 2)
+        self.assertEquals(errors[0], (0, 'image/png', 5, 'Image lacks a checksum. Generate pngs using run-webkit-tests to ensure they have a checksum.'))
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/style/checkers/python.py b/Tools/Scripts/webkitpy/style/checkers/python.py
new file mode 100644
index 0000000..8cfd1b2
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/python.py
@@ -0,0 +1,56 @@
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+"""Supports checking WebKit style in Python files."""
+
+from webkitpy.thirdparty.autoinstalled import pep8
+
+
+class PythonChecker(object):
+
+    """Processes text lines for checking style."""
+
+    def __init__(self, file_path, handle_style_error):
+        self._file_path = file_path
+        self._handle_style_error = handle_style_error
+
+    def check(self, lines):
+        # Initialize pep8.options, which is necessary for
+        # Checker.check_all() to execute.
+        pep8.process_options(arglist=[self._file_path])
+
+        checker = pep8.Checker(self._file_path)
+
+        def _pep8_handle_error(line_number, offset, text, check):
+            # FIXME: Incorporate the character offset into the error output.
+            #        This will require updating the error handler __call__
+            #        signature to include an optional "offset" parameter.
+            pep8_code = text[:4]
+            pep8_message = text[5:]
+
+            category = "pep8/" + pep8_code
+
+            self._handle_style_error(line_number, category, 5, pep8_message)
+
+        checker.report_error = _pep8_handle_error
+
+        errors = checker.check_all()
diff --git a/Tools/Scripts/webkitpy/style/checkers/python_unittest.py b/Tools/Scripts/webkitpy/style/checkers/python_unittest.py
new file mode 100644
index 0000000..e003eb8
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/python_unittest.py
@@ -0,0 +1,62 @@
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+"""Unit tests for python.py."""
+
+import os
+import unittest
+
+from python import PythonChecker
+
+
+class PythonCheckerTest(unittest.TestCase):
+
+    """Tests the PythonChecker class."""
+
+    def test_init(self):
+        """Test __init__() method."""
+        def _mock_handle_style_error(self):
+            pass
+
+        checker = PythonChecker("foo.txt", _mock_handle_style_error)
+        self.assertEquals(checker._file_path, "foo.txt")
+        self.assertEquals(checker._handle_style_error,
+                          _mock_handle_style_error)
+
+    def test_check(self):
+        """Test check() method."""
+        errors = []
+
+        def _mock_handle_style_error(line_number, category, confidence,
+                                     message):
+            error = (line_number, category, confidence, message)
+            errors.append(error)
+
+        current_dir = os.path.dirname(__file__)
+        file_path = os.path.join(current_dir, "python_unittest_input.py")
+
+        checker = PythonChecker(file_path, _mock_handle_style_error)
+        checker.check(lines=[])
+
+        self.assertEquals(len(errors), 1)
+        self.assertEquals(errors[0],
+                          (2, "pep8/W291", 5, "trailing whitespace"))
diff --git a/Tools/Scripts/webkitpy/style/checkers/python_unittest_input.py b/Tools/Scripts/webkitpy/style/checkers/python_unittest_input.py
new file mode 100644
index 0000000..9f1d118
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/python_unittest_input.py
@@ -0,0 +1,2 @@
+# This file is sample input for python_unittest.py and includes a single
+# error which is an extra space at the end of this line. 
diff --git a/Tools/Scripts/webkitpy/style/checkers/test_expectations.py b/Tools/Scripts/webkitpy/style/checkers/test_expectations.py
new file mode 100644
index 0000000..51b97be
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/test_expectations.py
@@ -0,0 +1,99 @@
+# 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.
+
+"""Checks WebKit style for test_expectations files."""
+
+import logging
+import optparse
+import os
+import re
+import sys
+
+from common import TabChecker
+from webkitpy.common.host import Host
+from webkitpy.layout_tests.models.test_expectations import TestExpectationParser
+
+
+_log = logging.getLogger(__name__)
+
+
+class TestExpectationsChecker(object):
+    """Processes TestExpectations lines for validating the syntax."""
+
+    categories = set(['test/expectations'])
+
+    def _determine_port_from_expectations_path(self, host, expectations_path):
+        # Pass a configuration to avoid calling default_configuration() when initializing the port (takes 0.5 seconds on a Mac Pro!).
+        options_wk1 = optparse.Values({'configuration': 'Release', 'webkit_test_runner': False})
+        options_wk2 = optparse.Values({'configuration': 'Release', 'webkit_test_runner': True})
+        for port_name in host.port_factory.all_port_names():
+            ports = [host.port_factory.get(port_name, options=options_wk1), host.port_factory.get(port_name, options=options_wk2)]
+            for port in ports:
+                for test_expectation_file in port.expectations_files():
+                    if test_expectation_file.replace(port.path_from_webkit_base() + host.filesystem.sep, '') == expectations_path:
+                        return port
+        return None
+
+    def __init__(self, file_path, handle_style_error, host=None):
+        self._file_path = file_path
+        self._handle_style_error = handle_style_error
+        self._handle_style_error.turn_off_line_filtering()
+        self._tab_checker = TabChecker(file_path, handle_style_error)
+
+        # FIXME: host should be a required parameter, not an optional one.
+        host = host or Host()
+        host.initialize_scm()
+
+        self._port_obj = self._determine_port_from_expectations_path(host, file_path)
+
+        # Suppress error messages of test_expectations module since they will be reported later.
+        log = logging.getLogger("webkitpy.layout_tests.layout_package.test_expectations")
+        log.setLevel(logging.CRITICAL)
+
+    def _handle_error_message(self, lineno, message, confidence):
+        pass
+
+    def check_test_expectations(self, expectations_str, tests=None):
+        parser = TestExpectationParser(self._port_obj, tests, allow_rebaseline_modifier=False)
+        expectations = parser.parse('expectations', expectations_str)
+
+        level = 5
+        for expectation_line in expectations:
+            for warning in expectation_line.warnings:
+                self._handle_style_error(expectation_line.line_number, 'test/expectations', level, warning)
+
+    def check_tabs(self, lines):
+        self._tab_checker.check(lines)
+
+    def check(self, lines):
+        expectations = '\n'.join(lines)
+        if self._port_obj:
+            self.check_test_expectations(expectations_str=expectations, tests=None)
+
+        # Warn tabs in lines as well
+        self.check_tabs(lines)
diff --git a/Tools/Scripts/webkitpy/style/checkers/test_expectations_unittest.py b/Tools/Scripts/webkitpy/style/checkers/test_expectations_unittest.py
new file mode 100644
index 0000000..1516de7
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/test_expectations_unittest.py
@@ -0,0 +1,120 @@
+#!/usr/bin/python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import sys
+import unittest
+
+from test_expectations import TestExpectationsChecker
+from webkitpy.common.host_mock import MockHost
+
+
+class ErrorCollector(object):
+    """An error handler class for unit tests."""
+
+    def __init__(self):
+        self._errors = []
+        self.turned_off_filtering = False
+
+    def turn_off_line_filtering(self):
+        self.turned_off_filtering = True
+
+    def __call__(self, lineno, category, confidence, message):
+        self._errors.append('%s  [%s] [%d]' % (message, category, confidence))
+        return True
+
+    def get_errors(self):
+        return ''.join(self._errors)
+
+    def reset_errors(self):
+        self._errors = []
+        self.turned_off_filtering = False
+
+
+class TestExpectationsTestCase(unittest.TestCase):
+    """TestCase for test_expectations.py"""
+
+    def setUp(self):
+        self._error_collector = ErrorCollector()
+        self._test_file = 'passes/text.html'
+
+    def _expect_port_for_expectations_path(self, expected_port_implementation, expectations_path):
+        host = MockHost()
+        checker = TestExpectationsChecker(expectations_path, ErrorCollector(), host=host)
+        port = checker._determine_port_from_expectations_path(host, expectations_path)
+        if port:
+            self.assertTrue(port.name().startswith(expected_port_implementation))
+        else:
+            self.assertEquals(None, expected_port_implementation)
+
+    def test_determine_port_from_expectations_path(self):
+        self._expect_port_for_expectations_path(None, '/')
+        self._expect_port_for_expectations_path(None, 'LayoutTests/chromium-mac/TestExpectations')
+        self._expect_port_for_expectations_path('chromium', 'LayoutTests/platform/chromium/TestExpectations')
+        self._expect_port_for_expectations_path(None, '/mock-checkout/LayoutTests/platform/win/TestExpectations')
+        self._expect_port_for_expectations_path('win', 'LayoutTests/platform/win/TestExpectations')
+        self._expect_port_for_expectations_path('efl', 'LayoutTests/platform/efl/TestExpectations')
+        self._expect_port_for_expectations_path('efl', 'LayoutTests/platform/efl-wk1/TestExpectations')
+        self._expect_port_for_expectations_path('efl', 'LayoutTests/platform/efl-wk2/TestExpectations')
+        self._expect_port_for_expectations_path('qt', 'LayoutTests/platform/qt-win/TestExpectations')
+        # FIXME: check-webkit-style doesn't know how to create port objects for all Qt version (4.8, 5.0) and
+        # will only check files based on the installed version of Qt.
+        #self._expect_port_for_expectations_path('qt', 'LayoutTests/platform/qt-5.0-wk2/TestExpectations')
+
+    def assert_lines_lint(self, lines, should_pass, expected_output=None):
+        self._error_collector.reset_errors()
+
+        host = MockHost()
+        checker = TestExpectationsChecker('test/TestExpectations',
+                                          self._error_collector, host=host)
+
+        # We should have failed to find a valid port object for that path.
+        self.assertEquals(checker._port_obj, None)
+
+        # Now use a test port so we can check the lines.
+        checker._port_obj = host.port_factory.get('test-mac-leopard')
+        checker.check_test_expectations(expectations_str='\n'.join(lines),
+                                        tests=[self._test_file])
+        checker.check_tabs(lines)
+        if should_pass:
+            self.assertEqual('', self._error_collector.get_errors())
+        elif expected_output:
+            self.assertEquals(expected_output, self._error_collector.get_errors())
+        else:
+            self.assertNotEquals('', self._error_collector.get_errors())
+        self.assertTrue(self._error_collector.turned_off_filtering)
+
+    def test_valid_expectations(self):
+        self.assert_lines_lint(["crbug.com/1234 [ Mac ] passes/text.html [ Pass Failure ]"], should_pass=True)
+
+    def test_invalid_expectations(self):
+        self.assert_lines_lint(["Bug(me) passes/text.html [ Give Up]"], should_pass=False)
+
+    def test_tab(self):
+        self.assert_lines_lint(["\twebkit.org/b/1 passes/text.html [ Pass ]"], should_pass=False, expected_output="Line contains tab character.  [whitespace/tab] [5]")
diff --git a/Tools/Scripts/webkitpy/style/checkers/text.py b/Tools/Scripts/webkitpy/style/checkers/text.py
new file mode 100644
index 0000000..1147658
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/text.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+"""Checks WebKit style for text files."""
+
+from common import TabChecker
+
+class TextChecker(object):
+
+    """Processes text lines for checking style."""
+
+    def __init__(self, file_path, handle_style_error):
+        self.file_path = file_path
+        self.handle_style_error = handle_style_error
+        self._tab_checker = TabChecker(file_path, handle_style_error)
+
+    def check(self, lines):
+        self._tab_checker.check(lines)
+
+
+# FIXME: Remove this function (requires refactoring unit tests).
+def process_file_data(filename, lines, error):
+    checker = TextChecker(filename, error)
+    checker.check(lines)
+
diff --git a/Tools/Scripts/webkitpy/style/checkers/text_unittest.py b/Tools/Scripts/webkitpy/style/checkers/text_unittest.py
new file mode 100644
index 0000000..ced49a9
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/text_unittest.py
@@ -0,0 +1,94 @@
+#!/usr/bin/python
+# Copyright (C) 2009 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.
+
+"""Unit test for text_style.py."""
+
+import unittest
+
+import text as text_style
+from text import TextChecker
+
+class TextStyleTestCase(unittest.TestCase):
+    """TestCase for text_style.py"""
+
+    def assertNoError(self, lines):
+        """Asserts that the specified lines has no errors."""
+        self.had_error = False
+
+        def error_for_test(line_number, category, confidence, message):
+            """Records if an error occurs."""
+            self.had_error = True
+
+        text_style.process_file_data('', lines, error_for_test)
+        self.assert_(not self.had_error, '%s should not have any errors.' % lines)
+
+    def assertError(self, lines, expected_line_number):
+        """Asserts that the specified lines has an error."""
+        self.had_error = False
+
+        def error_for_test(line_number, category, confidence, message):
+            """Checks if the expected error occurs."""
+            self.assertEquals(expected_line_number, line_number)
+            self.assertEquals('whitespace/tab', category)
+            self.had_error = True
+
+        text_style.process_file_data('', lines, error_for_test)
+        self.assert_(self.had_error, '%s should have an error [whitespace/tab].' % lines)
+
+
+    def test_no_error(self):
+        """Tests for no error cases."""
+        self.assertNoError([''])
+        self.assertNoError(['abc def', 'ggg'])
+
+
+    def test_error(self):
+        """Tests for error cases."""
+        self.assertError(['2009-12-16\tKent Tamura\t<tkent@chromium.org>'], 1)
+        self.assertError(['2009-12-16 Kent Tamura <tkent@chromium.org>',
+                          '',
+                          '\tReviewed by NOBODY.'], 3)
+
+
+class TextCheckerTest(unittest.TestCase):
+
+    """Tests TextChecker class."""
+
+    def mock_handle_style_error(self):
+        pass
+
+    def test_init(self):
+        """Test __init__ constructor."""
+        checker = TextChecker("foo.txt", self.mock_handle_style_error)
+        self.assertEquals(checker.file_path, "foo.txt")
+        self.assertEquals(checker.handle_style_error, self.mock_handle_style_error)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/style/checkers/watchlist.py b/Tools/Scripts/webkitpy/style/checkers/watchlist.py
new file mode 100644
index 0000000..d1a27f7
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/watchlist.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2011 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.
+
+
+"""Checks WebKit style for the watchlist file."""
+
+
+from webkitpy.common.watchlist.watchlistparser import WatchListParser
+
+
+class WatchListChecker(object):
+
+    """Processes the watch list for checking style."""
+
+    def __init__(self, file_path, handle_style_error):
+        self._handle_style_error = handle_style_error
+        self._handle_style_error.turn_off_line_filtering()
+
+    def check(self, lines):
+        def log_to_style_error(message):
+            # Always report line 0 since we don't have anything better.
+            self._handle_style_error(0,
+                                     'watchlist/general', 5,
+                                     message)
+
+        WatchListParser(log_error=log_to_style_error).parse('\n'.join(lines))
diff --git a/Tools/Scripts/webkitpy/style/checkers/watchlist_unittest.py b/Tools/Scripts/webkitpy/style/checkers/watchlist_unittest.py
new file mode 100644
index 0000000..c8d29db
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/watchlist_unittest.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2010 Apple Inc. All rights reserved.
+# Copyright (C) 2011 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.
+
+
+'''Unit tests for watchlist.py.'''
+
+
+import unittest
+
+
+import watchlist
+
+
+class MockErrorHandler(object):
+    def __init__(self, handle_style_error):
+        self.turned_off_filtering = False
+        self._handle_style_error = handle_style_error
+
+    def turn_off_line_filtering(self):
+        self.turned_off_filtering = True
+
+    def __call__(self, line_number, category, confidence, message):
+        self._handle_style_error(self, line_number, category, confidence, message)
+        return True
+
+
+class WatchListTest(unittest.TestCase):
+    def test_basic_error_message(self):
+        def handle_style_error(mock_error_handler, line_number, category, confidence, message):
+            mock_error_handler.had_error = True
+            self.assertEquals(0, line_number)
+            self.assertEquals('watchlist/general', category)
+
+        error_handler = MockErrorHandler(handle_style_error)
+        error_handler.had_error = False
+        checker = watchlist.WatchListChecker('watchlist', error_handler)
+        checker.check(['{"DEFINTIONS": {}}'])
+        self.assertTrue(error_handler.had_error)
+        self.assertTrue(error_handler.turned_off_filtering)
diff --git a/Tools/Scripts/webkitpy/style/checkers/xcodeproj.py b/Tools/Scripts/webkitpy/style/checkers/xcodeproj.py
new file mode 100644
index 0000000..89c072d
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/xcodeproj.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2011 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:
+# 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 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.
+
+"""Checks Xcode project files."""
+
+import re
+
+
+class XcodeProjectFileChecker(object):
+
+    """Processes Xcode project file lines for checking style."""
+
+    def __init__(self, file_path, handle_style_error):
+        self.file_path = file_path
+        self.handle_style_error = handle_style_error
+        self.handle_style_error.turn_off_line_filtering()
+        self._development_region_regex = re.compile('developmentRegion = (?P<region>.+);')
+
+    def _check_development_region(self, line_index, line):
+        """Returns True when developmentRegion is detected."""
+        matched = self._development_region_regex.search(line)
+        if not matched:
+            return False
+        if matched.group('region') != 'English':
+            self.handle_style_error(line_index,
+                                    'xcodeproj/settings', 5,
+                                    'developmentRegion is not English.')
+        return True
+
+    def check(self, lines):
+        development_region_is_detected = False
+        for line_index, line in enumerate(lines):
+            if self._check_development_region(line_index, line):
+                development_region_is_detected = True
+
+        if not development_region_is_detected:
+            self.handle_style_error(len(lines),
+                                    'xcodeproj/settings', 5,
+                                    'Missing "developmentRegion = English".')
diff --git a/Tools/Scripts/webkitpy/style/checkers/xcodeproj_unittest.py b/Tools/Scripts/webkitpy/style/checkers/xcodeproj_unittest.py
new file mode 100644
index 0000000..9799ec0
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/xcodeproj_unittest.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2011 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:
+# 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 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.
+
+"""Unit test for xcodeproj.py."""
+
+import xcodeproj
+import unittest
+
+
+class TestErrorHandler(object):
+    """Error handler for XcodeProjectFileChecker unittests"""
+    def __init__(self, handler):
+        self.handler = handler
+
+    def turn_off_line_filtering(self):
+        pass
+
+    def __call__(self, line_number, category, confidence, message):
+        self.handler(self, line_number, category, confidence, message)
+        return True
+
+
+class XcodeProjectFileCheckerTest(unittest.TestCase):
+    """Tests XcodeProjectFileChecker class."""
+
+    def assert_no_error(self, lines):
+        def handler(error_handler, line_number, category, confidence, message):
+            self.fail('Unexpected error: %d %s %d %s' % (line_number, category, confidence, message))
+
+        error_handler = TestErrorHandler(handler)
+        checker = xcodeproj.XcodeProjectFileChecker('', error_handler)
+        checker.check(lines)
+
+    def assert_error(self, lines, expected_message):
+        self.had_error = False
+
+        def handler(error_handler, line_number, category, confidence, message):
+            self.assertEqual(expected_message, message)
+            self.had_error = True
+        error_handler = TestErrorHandler(handler)
+        checker = xcodeproj.XcodeProjectFileChecker('', error_handler)
+        checker.check(lines)
+        self.assert_(self.had_error, '%s should have error: %s.' % (lines, expected_message))
+
+    def test_detect_development_region(self):
+        self.assert_no_error(['developmentRegion = English;'])
+        self.assert_error([''], 'Missing "developmentRegion = English".')
+        self.assert_error(['developmentRegion = Japanese;'],
+                          'developmentRegion is not English.')
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/style/checkers/xml.py b/Tools/Scripts/webkitpy/style/checkers/xml.py
new file mode 100644
index 0000000..ff4a415
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/xml.py
@@ -0,0 +1,45 @@
+# 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.
+
+"""Checks WebKit style for XML files."""
+
+from __future__ import absolute_import
+
+from xml.parsers import expat
+
+
+class XMLChecker(object):
+    """Processes XML lines for checking style."""
+
+    def __init__(self, file_path, handle_style_error):
+        self._handle_style_error = handle_style_error
+        self._handle_style_error.turn_off_line_filtering()
+
+    def check(self, lines):
+        parser = expat.ParserCreate()
+        try:
+            for line in lines:
+                parser.Parse(line)
+                parser.Parse('\n')
+            parser.Parse('', True)
+        except expat.ExpatError, error:
+            self._handle_style_error(error.lineno, 'xml/syntax', 5, expat.ErrorString(error.code))
diff --git a/Tools/Scripts/webkitpy/style/checkers/xml_unittest.py b/Tools/Scripts/webkitpy/style/checkers/xml_unittest.py
new file mode 100644
index 0000000..e486f5f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/checkers/xml_unittest.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python
+#
+# 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.
+
+"""Unit test for xml.py."""
+
+import unittest
+
+import xml
+
+
+class MockErrorHandler(object):
+    def __init__(self, handle_style_error):
+        self.turned_off_filtering = False
+        self._handle_style_error = handle_style_error
+
+    def turn_off_line_filtering(self):
+        self.turned_off_filtering = True
+
+    def __call__(self, line_number, category, confidence, message):
+        self._handle_style_error(self, line_number, category, confidence, message)
+        return True
+
+
+class XMLCheckerTest(unittest.TestCase):
+    """Tests XMLChecker class."""
+
+    def assert_no_error(self, xml_data):
+        def handle_style_error(mock_error_handler, line_number, category, confidence, message):
+            self.fail('Unexpected error: %d %s %d %s' % (line_number, category, confidence, message))
+
+        error_handler = MockErrorHandler(handle_style_error)
+        checker = xml.XMLChecker('foo.xml', error_handler)
+        checker.check(xml_data.split('\n'))
+        self.assertTrue(error_handler.turned_off_filtering)
+
+    def assert_error(self, expected_line_number, expected_category, xml_data):
+        def handle_style_error(mock_error_handler, line_number, category, confidence, message):
+            mock_error_handler.had_error = True
+            self.assertEquals(expected_line_number, line_number)
+            self.assertEquals(expected_category, category)
+
+        error_handler = MockErrorHandler(handle_style_error)
+        error_handler.had_error = False
+
+        checker = xml.XMLChecker('foo.xml', error_handler)
+        checker.check(xml_data.split('\n'))
+        self.assertTrue(error_handler.had_error)
+        self.assertTrue(error_handler.turned_off_filtering)
+
+    def mock_handle_style_error(self):
+        pass
+
+    def test_conflict_marker(self):
+        self.assert_error(1, 'xml/syntax', '<<<<<<< HEAD\n<foo>\n</foo>\n')
+
+    def test_extra_closing_tag(self):
+        self.assert_error(3, 'xml/syntax', '<foo>\n</foo>\n</foo>\n')
+
+    def test_init(self):
+        error_handler = MockErrorHandler(self.mock_handle_style_error)
+        checker = xml.XMLChecker('foo.xml', error_handler)
+        self.assertEquals(checker._handle_style_error, error_handler)
+
+    def test_missing_closing_tag(self):
+        self.assert_error(3, 'xml/syntax', '<foo>\n<bar>\n</foo>\n')
+
+    def test_no_error(self):
+        self.assert_no_error('<foo>\n</foo>')
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/style/error_handlers.py b/Tools/Scripts/webkitpy/style/error_handlers.py
new file mode 100644
index 0000000..99d5cb3
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/error_handlers.py
@@ -0,0 +1,164 @@
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+"""Defines style error handler classes.
+
+A style error handler is a function to call when a style error is
+found. Style error handlers can also have state. A class that represents
+a style error handler should implement the following methods.
+
+Methods:
+
+  __call__(self, line_number, category, confidence, message):
+
+    Handle the occurrence of a style error.
+
+    Check whether the error is reportable. If so, increment the total
+    error count and report the details. Note that error reporting can
+    be suppressed after reaching a certain number of reports.
+
+    Args:
+      line_number: The integer line number of the line containing the error.
+      category: The name of the category of the error, for example
+                "whitespace/newline".
+      confidence: An integer between 1 and 5 inclusive that represents the
+                  application's level of confidence in the error. The value
+                  5 means that we are certain of the problem, and the
+                  value 1 means that it could be a legitimate construct.
+      message: The error message to report.
+
+"""
+
+
+import sys
+
+
+class DefaultStyleErrorHandler(object):
+
+    """The default style error handler."""
+
+    def __init__(self, file_path, configuration, increment_error_count,
+                 line_numbers=None):
+        """Create a default style error handler.
+
+        Args:
+          file_path: The path to the file containing the error. This
+                     is used for reporting to the user.
+          configuration: A StyleProcessorConfiguration instance.
+          increment_error_count: A function that takes no arguments and
+                                 increments the total count of reportable
+                                 errors.
+          line_numbers: An array of line numbers of the lines for which
+                        style errors should be reported, or None if errors
+                        for all lines should be reported.  When it is not
+                        None, this array normally contains the line numbers
+                        corresponding to the modified lines of a patch.
+
+        """
+        if line_numbers is not None:
+            line_numbers = set(line_numbers)
+
+        self._file_path = file_path
+        self._configuration = configuration
+        self._increment_error_count = increment_error_count
+        self._line_numbers = line_numbers
+
+        # A string to integer dictionary cache of the number of reportable
+        # errors per category passed to this instance.
+        self._category_totals = {}
+
+    # Useful for unit testing.
+    def __eq__(self, other):
+        """Return whether this instance is equal to another."""
+        if self._configuration != other._configuration:
+            return False
+        if self._file_path != other._file_path:
+            return False
+        if self._increment_error_count != other._increment_error_count:
+            return False
+        if self._line_numbers != other._line_numbers:
+            return False
+
+        return True
+
+    # Useful for unit testing.
+    def __ne__(self, other):
+        # Python does not automatically deduce __ne__ from __eq__.
+        return not self.__eq__(other)
+
+    def _add_reportable_error(self, category):
+        """Increment the error count and return the new category total."""
+        self._increment_error_count() # Increment the total.
+
+        # Increment the category total.
+        if not category in self._category_totals:
+            self._category_totals[category] = 1
+        else:
+            self._category_totals[category] += 1
+
+        return self._category_totals[category]
+
+    def _max_reports(self, category):
+        """Return the maximum number of errors to report."""
+        if not category in self._configuration.max_reports_per_category:
+            return None
+        return self._configuration.max_reports_per_category[category]
+
+    def should_line_be_checked(self, line_number):
+        "Returns if a particular line should be checked"
+        # Was the line that was modified?
+        return self._line_numbers is None or line_number in self._line_numbers
+
+    def turn_off_line_filtering(self):
+        self._line_numbers = None
+
+    def __call__(self, line_number, category, confidence, message):
+        """Handle the occurrence of a style error.
+
+        See the docstring of this module for more information.
+
+        """
+        if not self.should_line_be_checked(line_number):
+            return False
+
+        if not self._configuration.is_reportable(category=category,
+                                                 confidence_in_error=confidence,
+                                                 file_path=self._file_path):
+            return False
+
+        category_total = self._add_reportable_error(category)
+
+        max_reports = self._max_reports(category)
+
+        if (max_reports is not None) and (category_total > max_reports):
+            # Then suppress displaying the error.
+            return False
+
+        self._configuration.write_style_error(category=category,
+                                              confidence_in_error=confidence,
+                                              file_path=self._file_path,
+                                              line_number=line_number,
+                                              message=message)
+        if category_total == max_reports:
+            self._configuration.stderr_write("Suppressing further [%s] reports "
+                                             "for this file.\n" % category)
+        return True
diff --git a/Tools/Scripts/webkitpy/style/error_handlers_unittest.py b/Tools/Scripts/webkitpy/style/error_handlers_unittest.py
new file mode 100644
index 0000000..864bc0f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/error_handlers_unittest.py
@@ -0,0 +1,196 @@
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+"""Unit tests for error_handlers.py."""
+
+
+import unittest
+
+from checker import StyleProcessorConfiguration
+from error_handlers import DefaultStyleErrorHandler
+from filter import FilterConfiguration
+
+
+class DefaultStyleErrorHandlerTest(unittest.TestCase):
+
+    """Tests the DefaultStyleErrorHandler class."""
+
+    def setUp(self):
+        self._error_messages = []
+        self._error_count = 0
+
+    _category = "whitespace/tab"
+    """The category name for the tests in this class."""
+
+    _file_path = "foo.h"
+    """The file path for the tests in this class."""
+
+    def _mock_increment_error_count(self):
+        self._error_count += 1
+
+    def _mock_stderr_write(self, message):
+        self._error_messages.append(message)
+
+    def _style_checker_configuration(self):
+        """Return a StyleProcessorConfiguration instance for testing."""
+        base_rules = ["-whitespace", "+whitespace/tab"]
+        filter_configuration = FilterConfiguration(base_rules=base_rules)
+
+        return StyleProcessorConfiguration(
+                   filter_configuration=filter_configuration,
+                   max_reports_per_category={"whitespace/tab": 2},
+                   min_confidence=3,
+                   output_format="vs7",
+                   stderr_write=self._mock_stderr_write)
+
+    def _error_handler(self, configuration, line_numbers=None):
+        return DefaultStyleErrorHandler(configuration=configuration,
+                   file_path=self._file_path,
+                   increment_error_count=self._mock_increment_error_count,
+                   line_numbers=line_numbers)
+
+    def _check_initialized(self):
+        """Check that count and error messages are initialized."""
+        self.assertEquals(0, self._error_count)
+        self.assertEquals(0, len(self._error_messages))
+
+    def _call_error_handler(self, handle_error, confidence, line_number=100):
+        """Call the given error handler with a test error."""
+        handle_error(line_number=line_number,
+                     category=self._category,
+                     confidence=confidence,
+                     message="message")
+
+    def test_eq__true_return_value(self):
+        """Test the __eq__() method for the return value of True."""
+        handler1 = self._error_handler(configuration=None)
+        handler2 = self._error_handler(configuration=None)
+
+        self.assertTrue(handler1.__eq__(handler2))
+
+    def test_eq__false_return_value(self):
+        """Test the __eq__() method for the return value of False."""
+        def make_handler(configuration=self._style_checker_configuration(),
+                file_path='foo.txt', increment_error_count=lambda: True,
+                line_numbers=[100]):
+            return DefaultStyleErrorHandler(configuration=configuration,
+                       file_path=file_path,
+                       increment_error_count=increment_error_count,
+                       line_numbers=line_numbers)
+
+        handler = make_handler()
+
+        # Establish a baseline for our comparisons below.
+        self.assertTrue(handler.__eq__(make_handler()))
+
+        # Verify that a difference in any argument causes equality to fail.
+        self.assertFalse(handler.__eq__(make_handler(configuration=None)))
+        self.assertFalse(handler.__eq__(make_handler(file_path='bar.txt')))
+        self.assertFalse(handler.__eq__(make_handler(increment_error_count=None)))
+        self.assertFalse(handler.__eq__(make_handler(line_numbers=[50])))
+
+    def test_ne(self):
+        """Test the __ne__() method."""
+        # By default, __ne__ always returns true on different objects.
+        # Thus, check just the distinguishing case to verify that the
+        # code defines __ne__.
+        handler1 = self._error_handler(configuration=None)
+        handler2 = self._error_handler(configuration=None)
+
+        self.assertFalse(handler1.__ne__(handler2))
+
+    def test_non_reportable_error(self):
+        """Test __call__() with a non-reportable error."""
+        self._check_initialized()
+        configuration = self._style_checker_configuration()
+
+        confidence = 1
+        # Confirm the error is not reportable.
+        self.assertFalse(configuration.is_reportable(self._category,
+                                                     confidence,
+                                                     self._file_path))
+        error_handler = self._error_handler(configuration)
+        self._call_error_handler(error_handler, confidence)
+
+        self.assertEquals(0, self._error_count)
+        self.assertEquals([], self._error_messages)
+
+    # Also serves as a reportable error test.
+    def test_max_reports_per_category(self):
+        """Test error report suppression in __call__() method."""
+        self._check_initialized()
+        configuration = self._style_checker_configuration()
+        error_handler = self._error_handler(configuration)
+
+        confidence = 5
+
+        # First call: usual reporting.
+        self._call_error_handler(error_handler, confidence)
+        self.assertEquals(1, self._error_count)
+        self.assertEquals(1, len(self._error_messages))
+        self.assertEquals(self._error_messages,
+                          ["foo.h(100):  message  [whitespace/tab] [5]\n"])
+
+        # Second call: suppression message reported.
+        self._call_error_handler(error_handler, confidence)
+        # The "Suppressing further..." message counts as an additional
+        # message (but not as an addition to the error count).
+        self.assertEquals(2, self._error_count)
+        self.assertEquals(3, len(self._error_messages))
+        self.assertEquals(self._error_messages[-2],
+                          "foo.h(100):  message  [whitespace/tab] [5]\n")
+        self.assertEquals(self._error_messages[-1],
+                          "Suppressing further [whitespace/tab] reports "
+                          "for this file.\n")
+
+        # Third call: no report.
+        self._call_error_handler(error_handler, confidence)
+        self.assertEquals(3, self._error_count)
+        self.assertEquals(3, len(self._error_messages))
+
+    def test_line_numbers(self):
+        """Test the line_numbers parameter."""
+        self._check_initialized()
+        configuration = self._style_checker_configuration()
+        error_handler = self._error_handler(configuration,
+                                            line_numbers=[50])
+        confidence = 5
+
+        # Error on non-modified line: no error.
+        self._call_error_handler(error_handler, confidence, line_number=60)
+        self.assertEquals(0, self._error_count)
+        self.assertEquals([], self._error_messages)
+
+        # Error on modified line: error.
+        self._call_error_handler(error_handler, confidence, line_number=50)
+        self.assertEquals(1, self._error_count)
+        self.assertEquals(self._error_messages,
+                          ["foo.h(50):  message  [whitespace/tab] [5]\n"])
+
+        # Error on non-modified line after turning off line filtering: error.
+        error_handler.turn_off_line_filtering()
+        self._call_error_handler(error_handler, confidence, line_number=60)
+        self.assertEquals(2, self._error_count)
+        self.assertEquals(self._error_messages,
+                          ['foo.h(50):  message  [whitespace/tab] [5]\n',
+                           'foo.h(60):  message  [whitespace/tab] [5]\n',
+                           'Suppressing further [whitespace/tab] reports for this file.\n'])
diff --git a/Tools/Scripts/webkitpy/style/filereader.py b/Tools/Scripts/webkitpy/style/filereader.py
new file mode 100644
index 0000000..1181ad4
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/filereader.py
@@ -0,0 +1,154 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+# Copyright (C) 2010 ProFUSION embedded systems
+#
+# 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.
+
+"""Supports reading and processing text files."""
+
+import codecs
+import logging
+import os
+import sys
+
+
+_log = logging.getLogger(__name__)
+
+
+class TextFileReader(object):
+
+    """Supports reading and processing text files.
+
+       Attributes:
+         file_count: The total number of files passed to this instance
+                     for processing, including non-text files and files
+                     that should be skipped.
+         delete_only_file_count: The total number of files that are not
+                                 processed this instance actually because
+                                 the files don't have any modified lines
+                                 but should be treated as processed.
+
+    """
+
+    def __init__(self, filesystem, processor):
+        """Create an instance.
+
+        Arguments:
+          processor: A ProcessorBase instance.
+
+        """
+        # FIXME: Although TextFileReader requires a FileSystem it circumvents it in two places!
+        self.filesystem = filesystem
+        self._processor = processor
+        self.file_count = 0
+        self.delete_only_file_count = 0
+
+    def _read_lines(self, file_path):
+        """Read the file at a path, and return its lines.
+
+        Raises:
+          IOError: If the file does not exist or cannot be read.
+
+        """
+        # Support the UNIX convention of using "-" for stdin.
+        if file_path == '-':
+            file = codecs.StreamReaderWriter(sys.stdin,
+                                             codecs.getreader('utf8'),
+                                             codecs.getwriter('utf8'),
+                                             'replace')
+        else:
+            # We do not open the file with universal newline support
+            # (codecs does not support it anyway), so the resulting
+            # lines contain trailing "\r" characters if we are reading
+            # a file with CRLF endings.
+            # FIXME: This should use self.filesystem
+            file = codecs.open(file_path, 'r', 'utf8', 'replace')
+
+        try:
+            contents = file.read()
+        finally:
+            file.close()
+
+        lines = contents.split('\n')
+        return lines
+
+    def process_file(self, file_path, **kwargs):
+        """Process the given file by calling the processor's process() method.
+
+        Args:
+          file_path: The path of the file to process.
+          **kwargs: Any additional keyword parameters that should be passed
+                    to the processor's process() method.  The process()
+                    method should support these keyword arguments.
+
+        Raises:
+          SystemExit: If no file at file_path exists.
+
+        """
+        self.file_count += 1
+
+        if not self.filesystem.exists(file_path) and file_path != "-":
+            _log.error("File does not exist: '%s'" % file_path)
+            sys.exit(1)  # FIXME: This should throw or return instead of exiting directly.
+
+        if not self._processor.should_process(file_path):
+            _log.debug("Skipping file: '%s'" % file_path)
+            return
+        _log.debug("Processing file: '%s'" % file_path)
+
+        try:
+            lines = self._read_lines(file_path)
+        except IOError, err:
+            message = ("Could not read file. Skipping: '%s'\n  %s" % (file_path, err))
+            _log.warn(message)
+            return
+
+        self._processor.process(lines, file_path, **kwargs)
+
+    def _process_directory(self, directory):
+        """Process all files in the given directory, recursively."""
+        # FIXME: We should consider moving to self.filesystem.files_under() (or adding walk() to FileSystem)
+        for dir_path, dir_names, file_names in os.walk(directory):
+            for file_name in file_names:
+                file_path = self.filesystem.join(dir_path, file_name)
+                self.process_file(file_path)
+
+    def process_paths(self, paths):
+        for path in paths:
+            if self.filesystem.isdir(path):
+                self._process_directory(directory=path)
+            else:
+                self.process_file(path)
+
+    def count_delete_only_file(self):
+        """Count up files that contains only deleted lines.
+
+        Files which has no modified or newly-added lines don't need
+        to check style, but should be treated as checked. For that
+        purpose, we just count up the number of such files.
+        """
+        self.delete_only_file_count += 1
diff --git a/Tools/Scripts/webkitpy/style/filereader_unittest.py b/Tools/Scripts/webkitpy/style/filereader_unittest.py
new file mode 100644
index 0000000..bcf94f3
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/filereader_unittest.py
@@ -0,0 +1,155 @@
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+import unittest
+
+from webkitpy.common.system.filesystem import FileSystem
+from webkitpy.common.system.logtesting import LoggingTestCase
+from webkitpy.style.checker import ProcessorBase
+from webkitpy.style.filereader import TextFileReader
+
+
+class TextFileReaderTest(LoggingTestCase):
+
+    class MockProcessor(ProcessorBase):
+
+        """A processor for test purposes.
+
+        This processor simply records the parameters passed to its process()
+        method for later checking by the unittest test methods.
+
+        """
+
+        def __init__(self):
+            self.processed = []
+            """The parameters passed for all calls to the process() method."""
+
+        def should_process(self, file_path):
+            return not file_path.endswith('should_not_process.txt')
+
+        def process(self, lines, file_path, test_kwarg=None):
+            self.processed.append((lines, file_path, test_kwarg))
+
+    def setUp(self):
+        LoggingTestCase.setUp(self)
+        # FIXME: This should be a MockFileSystem once TextFileReader is moved entirely on top of FileSystem.
+        self.filesystem = FileSystem()
+        self._temp_dir = str(self.filesystem.mkdtemp())
+        self._processor = TextFileReaderTest.MockProcessor()
+        self._file_reader = TextFileReader(self.filesystem, self._processor)
+
+    def tearDown(self):
+        LoggingTestCase.tearDown(self)
+        self.filesystem.rmtree(self._temp_dir)
+
+    def _create_file(self, rel_path, text):
+        """Create a file with given text and return the path to the file."""
+        # FIXME: There are better/more secure APIs for creating tmp file paths.
+        file_path = self.filesystem.join(self._temp_dir, rel_path)
+        self.filesystem.write_text_file(file_path, text)
+        return file_path
+
+    def _passed_to_processor(self):
+        """Return the parameters passed to MockProcessor.process()."""
+        return self._processor.processed
+
+    def _assert_file_reader(self, passed_to_processor, file_count):
+        """Assert the state of the file reader."""
+        self.assertEquals(passed_to_processor, self._passed_to_processor())
+        self.assertEquals(file_count, self._file_reader.file_count)
+
+    def test_process_file__does_not_exist(self):
+        try:
+            self._file_reader.process_file('does_not_exist.txt')
+        except SystemExit, err:
+            self.assertEquals(str(err), '1')
+        else:
+            self.fail('No Exception raised.')
+        self._assert_file_reader([], 1)
+        self.assertLog(["ERROR: File does not exist: 'does_not_exist.txt'\n"])
+
+    def test_process_file__is_dir(self):
+        temp_dir = self.filesystem.join(self._temp_dir, 'test_dir')
+        self.filesystem.maybe_make_directory(temp_dir)
+
+        self._file_reader.process_file(temp_dir)
+
+        # Because the log message below contains exception text, it is
+        # possible that the text varies across platforms.  For this reason,
+        # we check only the portion of the log message that we control,
+        # namely the text at the beginning.
+        log_messages = self.logMessages()
+        # We remove the message we are looking at to prevent the tearDown()
+        # from raising an exception when it asserts that no log messages
+        # remain.
+        message = log_messages.pop()
+
+        self.assertTrue(message.startswith("WARNING: Could not read file. Skipping: '%s'\n  " % temp_dir))
+
+        self._assert_file_reader([], 1)
+
+    def test_process_file__should_not_process(self):
+        file_path = self._create_file('should_not_process.txt', 'contents')
+
+        self._file_reader.process_file(file_path)
+        self._assert_file_reader([], 1)
+
+    def test_process_file__multiple_lines(self):
+        file_path = self._create_file('foo.txt', 'line one\r\nline two\n')
+
+        self._file_reader.process_file(file_path)
+        processed = [(['line one\r', 'line two', ''], file_path, None)]
+        self._assert_file_reader(processed, 1)
+
+    def test_process_file__file_stdin(self):
+        file_path = self._create_file('-', 'file contents')
+
+        self._file_reader.process_file(file_path=file_path, test_kwarg='foo')
+        processed = [(['file contents'], file_path, 'foo')]
+        self._assert_file_reader(processed, 1)
+
+    def test_process_file__with_kwarg(self):
+        file_path = self._create_file('foo.txt', 'file contents')
+
+        self._file_reader.process_file(file_path=file_path, test_kwarg='foo')
+        processed = [(['file contents'], file_path, 'foo')]
+        self._assert_file_reader(processed, 1)
+
+    def test_process_paths(self):
+        # We test a list of paths that contains both a file and a directory.
+        dir = self.filesystem.join(self._temp_dir, 'foo_dir')
+        self.filesystem.maybe_make_directory(dir)
+
+        file_path1 = self._create_file('file1.txt', 'foo')
+
+        rel_path = self.filesystem.join('foo_dir', 'file2.txt')
+        file_path2 = self._create_file(rel_path, 'bar')
+
+        self._file_reader.process_paths([dir, file_path1])
+        processed = [(['bar'], file_path2, None),
+                     (['foo'], file_path1, None)]
+        self._assert_file_reader(processed, 2)
+
+    def test_count_delete_only_file(self):
+        self._file_reader.count_delete_only_file()
+        delete_only_file_count = self._file_reader.delete_only_file_count
+        self.assertEquals(delete_only_file_count, 1)
diff --git a/Tools/Scripts/webkitpy/style/filter.py b/Tools/Scripts/webkitpy/style/filter.py
new file mode 100644
index 0000000..608a9e6
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/filter.py
@@ -0,0 +1,278 @@
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+#
+# 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.
+
+"""Contains filter-related code."""
+
+
+def validate_filter_rules(filter_rules, all_categories):
+    """Validate the given filter rules, and raise a ValueError if not valid.
+
+    Args:
+      filter_rules: A list of boolean filter rules, for example--
+                    ["-whitespace", "+whitespace/braces"]
+      all_categories: A list of all available category names, for example--
+                      ["whitespace/tabs", "whitespace/braces"]
+
+    Raises:
+      ValueError: An error occurs if a filter rule does not begin
+                  with "+" or "-" or if a filter rule does not match
+                  the beginning of some category name in the list
+                  of all available categories.
+
+    """
+    for rule in filter_rules:
+        if not (rule.startswith('+') or rule.startswith('-')):
+            raise ValueError('Invalid filter rule "%s": every rule '
+                             "must start with + or -." % rule)
+
+        for category in all_categories:
+            if category.startswith(rule[1:]):
+                break
+        else:
+            raise ValueError('Suspected incorrect filter rule "%s": '
+                             "the rule does not match the beginning "
+                             "of any category name." % rule)
+
+
+class _CategoryFilter(object):
+
+    """Filters whether to check style categories."""
+
+    def __init__(self, filter_rules=None):
+        """Create a category filter.
+
+        Args:
+          filter_rules: A list of strings that are filter rules, which
+                        are strings beginning with the plus or minus
+                        symbol (+/-).  The list should include any
+                        default filter rules at the beginning.
+                        Defaults to the empty list.
+
+        Raises:
+          ValueError: Invalid filter rule if a rule does not start with
+                      plus ("+") or minus ("-").
+
+        """
+        if filter_rules is None:
+            filter_rules = []
+
+        self._filter_rules = filter_rules
+        self._should_check_category = {} # Cached dictionary of category to True/False
+
+    def __str__(self):
+        return ",".join(self._filter_rules)
+
+    # Useful for unit testing.
+    def __eq__(self, other):
+        """Return whether this CategoryFilter instance is equal to another."""
+        return self._filter_rules == other._filter_rules
+
+    # Useful for unit testing.
+    def __ne__(self, other):
+        # Python does not automatically deduce from __eq__().
+        return not (self == other)
+
+    def should_check(self, category):
+        """Return whether the category should be checked.
+
+        The rules for determining whether a category should be checked
+        are as follows.  By default all categories should be checked.
+        Then apply the filter rules in order from first to last, with
+        later flags taking precedence.
+
+        A filter rule applies to a category if the string after the
+        leading plus/minus (+/-) matches the beginning of the category
+        name.  A plus (+) means the category should be checked, while a
+        minus (-) means the category should not be checked.
+
+        """
+        if category in self._should_check_category:
+            return self._should_check_category[category]
+
+        should_check = True # All categories checked by default.
+        for rule in self._filter_rules:
+            if not category.startswith(rule[1:]):
+                continue
+            should_check = rule.startswith('+')
+        self._should_check_category[category] = should_check # Update cache.
+        return should_check
+
+
+class FilterConfiguration(object):
+
+    """Supports filtering with path-specific and user-specified rules."""
+
+    def __init__(self, base_rules=None, path_specific=None, user_rules=None):
+        """Create a FilterConfiguration instance.
+
+        Args:
+          base_rules: The starting list of filter rules to use for
+                      processing.  The default is the empty list, which
+                      by itself would mean that all categories should be
+                      checked.
+
+          path_specific: A list of (sub_paths, path_rules) pairs
+                         that stores the path-specific filter rules for
+                         appending to the base rules.
+                             The "sub_paths" value is a list of path
+                         substrings.  If a file path contains one of the
+                         substrings, then the corresponding path rules
+                         are appended.  The first substring match takes
+                         precedence, i.e. only the first match triggers
+                         an append.
+                             The "path_rules" value is a list of filter
+                         rules that can be appended to the base rules.
+
+          user_rules: A list of filter rules that is always appended
+                      to the base rules and any path rules.  In other
+                      words, the user rules take precedence over the
+                      everything.  In practice, the user rules are
+                      provided by the user from the command line.
+
+        """
+        if base_rules is None:
+            base_rules = []
+        if path_specific is None:
+            path_specific = []
+        if user_rules is None:
+            user_rules = []
+
+        self._base_rules = base_rules
+        self._path_specific = path_specific
+        self._path_specific_lower = None
+        """The backing store for self._get_path_specific_lower()."""
+
+        self._user_rules = user_rules
+
+        self._path_rules_to_filter = {}
+        """Cached dictionary of path rules to CategoryFilter instance."""
+
+        # The same CategoryFilter instance can be shared across
+        # multiple keys in this dictionary.  This allows us to take
+        # greater advantage of the caching done by
+        # CategoryFilter.should_check().
+        self._path_to_filter = {}
+        """Cached dictionary of file path to CategoryFilter instance."""
+
+    # Useful for unit testing.
+    def __eq__(self, other):
+        """Return whether this FilterConfiguration is equal to another."""
+        if self._base_rules != other._base_rules:
+            return False
+        if self._path_specific != other._path_specific:
+            return False
+        if self._user_rules != other._user_rules:
+            return False
+
+        return True
+
+    # Useful for unit testing.
+    def __ne__(self, other):
+        # Python does not automatically deduce this from __eq__().
+        return not self.__eq__(other)
+
+    # We use the prefix "_get" since the name "_path_specific_lower"
+    # is already taken up by the data attribute backing store.
+    def _get_path_specific_lower(self):
+        """Return a copy of self._path_specific with the paths lower-cased."""
+        if self._path_specific_lower is None:
+            self._path_specific_lower = []
+            for (sub_paths, path_rules) in self._path_specific:
+                sub_paths = map(str.lower, sub_paths)
+                self._path_specific_lower.append((sub_paths, path_rules))
+        return self._path_specific_lower
+
+    def _path_rules_from_path(self, path):
+        """Determine the path-specific rules to use, and return as a tuple.
+
+         This method returns a tuple rather than a list so the return
+         value can be passed to _filter_from_path_rules() without change.
+
+        """
+        path = path.lower()
+        for (sub_paths, path_rules) in self._get_path_specific_lower():
+            for sub_path in sub_paths:
+                if path.find(sub_path) > -1:
+                    return tuple(path_rules)
+        return () # Default to the empty tuple.
+
+    def _filter_from_path_rules(self, path_rules):
+        """Return the CategoryFilter associated to the given path rules.
+
+        Args:
+          path_rules: A tuple of path rules.  We require a tuple rather
+                      than a list so the value can be used as a dictionary
+                      key in self._path_rules_to_filter.
+
+        """
+        # We reuse the same CategoryFilter where possible to take
+        # advantage of the caching they do.
+        if path_rules not in self._path_rules_to_filter:
+            rules = list(self._base_rules) # Make a copy
+            rules.extend(path_rules)
+            rules.extend(self._user_rules)
+            self._path_rules_to_filter[path_rules] = _CategoryFilter(rules)
+
+        return self._path_rules_to_filter[path_rules]
+
+    def _filter_from_path(self, path):
+        """Return the CategoryFilter associated to a path."""
+        if path not in self._path_to_filter:
+            path_rules = self._path_rules_from_path(path)
+            filter = self._filter_from_path_rules(path_rules)
+            self._path_to_filter[path] = filter
+
+        return self._path_to_filter[path]
+
+    def should_check(self, category, path):
+        """Return whether the given category should be checked.
+
+        This method determines whether a category should be checked
+        by checking the category name against the filter rules for
+        the given path.
+
+        For a given path, the filter rules are the combination of
+        the base rules, the path-specific rules, and the user-provided
+        rules -- in that order.  As we will describe below, later rules
+        in the list take precedence.  The path-specific rules are the
+        rules corresponding to the first element of the "path_specific"
+        parameter that contains a string case-insensitively matching
+        some substring of the path.  If there is no such element,
+        there are no path-specific rules for that path.
+
+        Given a list of filter rules, the logic for determining whether
+        a category should be checked is as follows.  By default all
+        categories should be checked.  Then apply the filter rules in
+        order from first to last, with later flags taking precedence.
+
+        A filter rule applies to a category if the string after the
+        leading plus/minus (+/-) matches the beginning of the category
+        name.  A plus (+) means the category should be checked, while a
+        minus (-) means the category should not be checked.
+
+        Args:
+          category: The category name.
+          path: The path of the file being checked.
+
+        """
+        return self._filter_from_path(path).should_check(category)
+
diff --git a/Tools/Scripts/webkitpy/style/filter_unittest.py b/Tools/Scripts/webkitpy/style/filter_unittest.py
new file mode 100644
index 0000000..7b8a540
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/filter_unittest.py
@@ -0,0 +1,256 @@
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+#
+# 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.
+
+"""Unit tests for filter.py."""
+
+import unittest
+
+from filter import _CategoryFilter as CategoryFilter
+from filter import validate_filter_rules
+from filter import FilterConfiguration
+
+# On Testing __eq__() and __ne__():
+#
+# In the tests below, we deliberately do not use assertEquals() or
+# assertNotEquals() to test __eq__() or __ne__().  We do this to be
+# very explicit about what we are testing, especially in the case
+# of assertNotEquals().
+#
+# Part of the reason is that it is not immediately clear what
+# expression the unittest module uses to assert "not equals" -- the
+# negation of __eq__() or __ne__(), which are not necessarily
+# equivalent expresions in Python.  For example, from Python's "Data
+# Model" documentation--
+#
+#   "There are no implied relationships among the comparison
+#    operators. The truth of x==y does not imply that x!=y is
+#    false.  Accordingly, when defining __eq__(), one should
+#    also define __ne__() so that the operators will behave as
+#    expected."
+#
+#   (from http://docs.python.org/reference/datamodel.html#object.__ne__ )
+
+class ValidateFilterRulesTest(unittest.TestCase):
+
+    """Tests validate_filter_rules() function."""
+
+    def test_validate_filter_rules(self):
+        all_categories = ["tabs", "whitespace", "build/include"]
+
+        bad_rules = [
+            "tabs",
+            "*tabs",
+            " tabs",
+            " +tabs",
+            "+whitespace/newline",
+            "+xxx",
+            ]
+
+        good_rules = [
+            "+tabs",
+            "-tabs",
+            "+build"
+            ]
+
+        for rule in bad_rules:
+            self.assertRaises(ValueError, validate_filter_rules,
+                              [rule], all_categories)
+
+        for rule in good_rules:
+            # This works: no error.
+            validate_filter_rules([rule], all_categories)
+
+
+class CategoryFilterTest(unittest.TestCase):
+
+    """Tests CategoryFilter class."""
+
+    def test_init(self):
+        """Test __init__ method."""
+        # Test that the attributes are getting set correctly.
+        filter = CategoryFilter(["+"])
+        self.assertEquals(["+"], filter._filter_rules)
+
+    def test_init_default_arguments(self):
+        """Test __init__ method default arguments."""
+        filter = CategoryFilter()
+        self.assertEquals([], filter._filter_rules)
+
+    def test_str(self):
+        """Test __str__ "to string" operator."""
+        filter = CategoryFilter(["+a", "-b"])
+        self.assertEquals(str(filter), "+a,-b")
+
+    def test_eq(self):
+        """Test __eq__ equality function."""
+        filter1 = CategoryFilter(["+a", "+b"])
+        filter2 = CategoryFilter(["+a", "+b"])
+        filter3 = CategoryFilter(["+b", "+a"])
+
+        # See the notes at the top of this module about testing
+        # __eq__() and __ne__().
+        self.assertTrue(filter1.__eq__(filter2))
+        self.assertFalse(filter1.__eq__(filter3))
+
+    def test_ne(self):
+        """Test __ne__ inequality function."""
+        # By default, __ne__ always returns true on different objects.
+        # Thus, just check the distinguishing case to verify that the
+        # code defines __ne__.
+        #
+        # Also, see the notes at the top of this module about testing
+        # __eq__() and __ne__().
+        self.assertFalse(CategoryFilter().__ne__(CategoryFilter()))
+
+    def test_should_check(self):
+        """Test should_check() method."""
+        filter = CategoryFilter()
+        self.assertTrue(filter.should_check("everything"))
+        # Check a second time to exercise cache.
+        self.assertTrue(filter.should_check("everything"))
+
+        filter = CategoryFilter(["-"])
+        self.assertFalse(filter.should_check("anything"))
+        # Check a second time to exercise cache.
+        self.assertFalse(filter.should_check("anything"))
+
+        filter = CategoryFilter(["-", "+ab"])
+        self.assertTrue(filter.should_check("abc"))
+        self.assertFalse(filter.should_check("a"))
+
+        filter = CategoryFilter(["+", "-ab"])
+        self.assertFalse(filter.should_check("abc"))
+        self.assertTrue(filter.should_check("a"))
+
+
+class FilterConfigurationTest(unittest.TestCase):
+
+    """Tests FilterConfiguration class."""
+
+    def _config(self, base_rules, path_specific, user_rules):
+        """Return a FilterConfiguration instance."""
+        return FilterConfiguration(base_rules=base_rules,
+                                   path_specific=path_specific,
+                                   user_rules=user_rules)
+
+    def test_init(self):
+        """Test __init__ method."""
+        # Test that the attributes are getting set correctly.
+        # We use parameter values that are different from the defaults.
+        base_rules = ["-"]
+        path_specific = [(["path"], ["+a"])]
+        user_rules = ["+"]
+
+        config = self._config(base_rules, path_specific, user_rules)
+
+        self.assertEquals(base_rules, config._base_rules)
+        self.assertEquals(path_specific, config._path_specific)
+        self.assertEquals(user_rules, config._user_rules)
+
+    def test_default_arguments(self):
+        # Test that the attributes are getting set correctly to the defaults.
+        config = FilterConfiguration()
+
+        self.assertEquals([], config._base_rules)
+        self.assertEquals([], config._path_specific)
+        self.assertEquals([], config._user_rules)
+
+    def test_eq(self):
+        """Test __eq__ method."""
+        # See the notes at the top of this module about testing
+        # __eq__() and __ne__().
+        self.assertTrue(FilterConfiguration().__eq__(FilterConfiguration()))
+
+        # Verify that a difference in any argument causes equality to fail.
+        config = FilterConfiguration()
+
+        # These parameter values are different from the defaults.
+        base_rules = ["-"]
+        path_specific = [(["path"], ["+a"])]
+        user_rules = ["+"]
+
+        self.assertFalse(config.__eq__(FilterConfiguration(
+                                           base_rules=base_rules)))
+        self.assertFalse(config.__eq__(FilterConfiguration(
+                                           path_specific=path_specific)))
+        self.assertFalse(config.__eq__(FilterConfiguration(
+                                           user_rules=user_rules)))
+
+    def test_ne(self):
+        """Test __ne__ method."""
+        # By default, __ne__ always returns true on different objects.
+        # Thus, just check the distinguishing case to verify that the
+        # code defines __ne__.
+        #
+        # Also, see the notes at the top of this module about testing
+        # __eq__() and __ne__().
+        self.assertFalse(FilterConfiguration().__ne__(FilterConfiguration()))
+
+    def test_base_rules(self):
+        """Test effect of base_rules on should_check()."""
+        base_rules = ["-b"]
+        path_specific = []
+        user_rules = []
+
+        config = self._config(base_rules, path_specific, user_rules)
+
+        self.assertTrue(config.should_check("a", "path"))
+        self.assertFalse(config.should_check("b", "path"))
+
+    def test_path_specific(self):
+        """Test effect of path_rules_specifier on should_check()."""
+        base_rules = ["-"]
+        path_specific = [(["path1"], ["+b"]),
+                         (["path2"], ["+c"])]
+        user_rules = []
+
+        config = self._config(base_rules, path_specific, user_rules)
+
+        self.assertFalse(config.should_check("c", "path1"))
+        self.assertTrue(config.should_check("c", "path2"))
+        # Test that first match takes precedence.
+        self.assertFalse(config.should_check("c", "path2/path1"))
+
+    def test_path_with_different_case(self):
+        """Test a path that differs only in case."""
+        base_rules = ["-"]
+        path_specific = [(["Foo/"], ["+whitespace"])]
+        user_rules = []
+
+        config = self._config(base_rules, path_specific, user_rules)
+
+        self.assertFalse(config.should_check("whitespace", "Fooo/bar.txt"))
+        self.assertTrue(config.should_check("whitespace", "Foo/bar.txt"))
+        # Test different case.
+        self.assertTrue(config.should_check("whitespace", "FOO/bar.txt"))
+
+    def test_user_rules(self):
+        """Test effect of user_rules on should_check()."""
+        base_rules = ["-"]
+        path_specific = []
+        user_rules = ["+b"]
+
+        config = self._config(base_rules, path_specific, user_rules)
+
+        self.assertFalse(config.should_check("a", "path"))
+        self.assertTrue(config.should_check("b", "path"))
+
diff --git a/Tools/Scripts/webkitpy/style/main.py b/Tools/Scripts/webkitpy/style/main.py
new file mode 100644
index 0000000..574368a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/main.py
@@ -0,0 +1,162 @@
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+import codecs
+import logging
+import sys
+
+import webkitpy.style.checker as checker
+from webkitpy.style.patchreader import PatchReader
+from webkitpy.style.checker import StyleProcessor
+from webkitpy.style.filereader import TextFileReader
+from webkitpy.common.host import Host
+
+
+_log = logging.getLogger(__name__)
+
+
+def change_directory(filesystem, checkout_root, paths):
+    """Change the working directory to the WebKit checkout root, if possible.
+
+    If every path in the paths parameter is below the checkout root (or if
+    the paths parameter is empty or None), this method changes the current
+    working directory to the checkout root and converts the paths parameter
+    as described below.
+        This allows the paths being checked to be displayed relative to the
+    checkout root, and for path-specific style checks to work as expected.
+    Path-specific checks include whether files should be skipped, whether
+    custom style rules should apply to certain files, etc.
+
+    Returns:
+      paths: A copy of the paths parameter -- possibly converted, as follows.
+             If this method changed the current working directory to the
+             checkout root, then the list is the paths parameter converted to
+             normalized paths relative to the checkout root.
+
+    Args:
+      paths: A list of paths to the files that should be checked for style.
+             This argument can be None or the empty list if a git commit
+             or all changes under the checkout root should be checked.
+      checkout_root: The path to the root of the WebKit checkout.
+
+    """
+    if paths is not None:
+        paths = list(paths)
+
+    if paths:
+        # Then try converting all of the paths to paths relative to
+        # the checkout root.
+        rel_paths = []
+        for path in paths:
+            rel_path = filesystem.relpath(path, checkout_root)
+            if rel_path.startswith(filesystem.pardir):
+                # Then the path is not below the checkout root.  Since all
+                # paths should be interpreted relative to the same root,
+                # do not interpret any of the paths as relative to the
+                # checkout root.  Interpret all of them relative to the
+                # current working directory, and do not change the current
+                # working directory.
+                _log.warn(
+"""Path-dependent style checks may not work correctly:
+
+  One of the given paths is outside the WebKit checkout of the current
+  working directory:
+
+    Path: %s
+    Checkout root: %s
+
+  Pass only files below the checkout root to ensure correct results.
+  See the help documentation for more info.
+"""
+                          % (path, checkout_root))
+
+                return paths
+            rel_paths.append(rel_path)
+        # If we got here, the conversion was successful.
+        paths = rel_paths
+
+    _log.debug("Changing to checkout root: " + checkout_root)
+    filesystem.chdir(checkout_root)
+
+    return paths
+
+
+class CheckWebKitStyle(object):
+    def _engage_awesome_stderr_hacks(self):
+        # Change stderr to write with replacement characters so we don't die
+        # if we try to print something containing non-ASCII characters.
+        stderr = codecs.StreamReaderWriter(sys.stderr,
+                                           codecs.getreader('utf8'),
+                                           codecs.getwriter('utf8'),
+                                           'replace')
+        # Setting an "encoding" attribute on the stream is necessary to
+        # prevent the logging module from raising an error.  See
+        # the checker.configure_logging() function for more information.
+        stderr.encoding = "UTF-8"
+
+        # FIXME: Change webkitpy.style so that we do not need to overwrite
+        #        the global sys.stderr.  This involves updating the code to
+        #        accept a stream parameter where necessary, and not calling
+        #        sys.stderr explicitly anywhere.
+        sys.stderr = stderr
+        return stderr
+
+    def main(self):
+        args = sys.argv[1:]
+
+        host = Host()
+        host.initialize_scm()
+
+        stderr = self._engage_awesome_stderr_hacks()
+
+        # Checking for the verbose flag before calling check_webkit_style_parser()
+        # lets us enable verbose logging earlier.
+        is_verbose = "-v" in args or "--verbose" in args
+
+        checker.configure_logging(stream=stderr, is_verbose=is_verbose)
+        _log.debug("Verbose logging enabled.")
+
+        parser = checker.check_webkit_style_parser()
+        (paths, options) = parser.parse(args)
+
+        configuration = checker.check_webkit_style_configuration(options)
+
+        paths = change_directory(host.filesystem, checkout_root=host.scm().checkout_root, paths=paths)
+
+        style_processor = StyleProcessor(configuration)
+        file_reader = TextFileReader(host.filesystem, style_processor)
+
+        if paths and not options.diff_files:
+            file_reader.process_paths(paths)
+        else:
+            changed_files = paths if options.diff_files else None
+            patch = host.scm().create_patch(options.git_commit, changed_files=changed_files)
+            patch_checker = PatchReader(file_reader)
+            patch_checker.check(patch)
+
+        error_count = style_processor.error_count
+        file_count = file_reader.file_count
+        delete_only_file_count = file_reader.delete_only_file_count
+
+        _log.info("Total errors found: %d in %d files" % (error_count, file_count))
+        # We fail when style errors are found or there are no checked files.
+        return error_count > 0 or (file_count == 0 and delete_only_file_count == 0)
diff --git a/Tools/Scripts/webkitpy/style/main_unittest.py b/Tools/Scripts/webkitpy/style/main_unittest.py
new file mode 100644
index 0000000..5457833
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/main_unittest.py
@@ -0,0 +1,75 @@
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+import unittest
+
+from main import change_directory
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.logtesting import LogTesting
+
+
+class ChangeDirectoryTest(unittest.TestCase):
+    _original_directory = "/original"
+    _checkout_root = "/WebKit"
+
+    def setUp(self):
+        self._log = LogTesting.setUp(self)
+        self.filesystem = MockFileSystem(dirs=[self._original_directory, self._checkout_root], cwd=self._original_directory)
+
+    def tearDown(self):
+        self._log.tearDown()
+
+    def _change_directory(self, paths, checkout_root):
+        return change_directory(self.filesystem, paths=paths, checkout_root=checkout_root)
+
+    def _assert_result(self, actual_return_value, expected_return_value,
+                       expected_log_messages, expected_current_directory):
+        self.assertEquals(actual_return_value, expected_return_value)
+        self._log.assertMessages(expected_log_messages)
+        self.assertEquals(self.filesystem.getcwd(), expected_current_directory)
+
+    def test_paths_none(self):
+        paths = self._change_directory(checkout_root=self._checkout_root, paths=None)
+        self._assert_result(paths, None, [], self._checkout_root)
+
+    def test_paths_convertible(self):
+        paths = ["/WebKit/foo1.txt", "/WebKit/foo2.txt"]
+        paths = self._change_directory(checkout_root=self._checkout_root, paths=paths)
+        self._assert_result(paths, ["foo1.txt", "foo2.txt"], [], self._checkout_root)
+
+    def test_with_scm_paths_unconvertible(self):
+        paths = ["/WebKit/foo1.txt", "/outside/foo2.txt"]
+        paths = self._change_directory(checkout_root=self._checkout_root, paths=paths)
+        log_messages = [
+"""WARNING: Path-dependent style checks may not work correctly:
+
+  One of the given paths is outside the WebKit checkout of the current
+  working directory:
+
+    Path: /outside/foo2.txt
+    Checkout root: /WebKit
+
+  Pass only files below the checkout root to ensure correct results.
+  See the help documentation for more info.
+
+"""]
+        self._assert_result(paths, paths, log_messages, self._original_directory)
diff --git a/Tools/Scripts/webkitpy/style/optparser.py b/Tools/Scripts/webkitpy/style/optparser.py
new file mode 100644
index 0000000..f4e9923
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/optparser.py
@@ -0,0 +1,457 @@
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+"""Supports the parsing of command-line options for check-webkit-style."""
+
+import logging
+from optparse import OptionParser
+import os.path
+import sys
+
+from filter import validate_filter_rules
+# This module should not import anything from checker.py.
+
+_log = logging.getLogger(__name__)
+
+_USAGE = """usage: %prog [--help] [options] [path1] [path2] ...
+
+Overview:
+  Check coding style according to WebKit style guidelines:
+
+      http://webkit.org/coding/coding-style.html
+
+  Path arguments can be files and directories.  If neither a git commit nor
+  paths are passed, then all changes in your source control working directory
+  are checked.
+
+Style errors:
+  This script assigns to every style error a confidence score from 1-5 and
+  a category name.  A confidence score of 5 means the error is certainly
+  a problem, and 1 means it could be fine.
+
+  Category names appear in error messages in brackets, for example
+  [whitespace/indent].  See the options section below for an option that
+  displays all available categories and which are reported by default.
+
+Filters:
+  Use filters to configure what errors to report.  Filters are specified using
+  a comma-separated list of boolean filter rules.  The script reports errors
+  in a category if the category passes the filter, as described below.
+
+  All categories start out passing.  Boolean filter rules are then evaluated
+  from left to right, with later rules taking precedence.  For example, the
+  rule "+foo" passes any category that starts with "foo", and "-foo" fails
+  any such category.  The filter input "-whitespace,+whitespace/braces" fails
+  the category "whitespace/tab" and passes "whitespace/braces".
+
+  Examples: --filter=-whitespace,+whitespace/braces
+            --filter=-whitespace,-runtime/printf,+runtime/printf_format
+            --filter=-,+build/include_what_you_use
+
+Paths:
+  Certain style-checking behavior depends on the paths relative to
+  the WebKit source root of the files being checked.  For example,
+  certain types of errors may be handled differently for files in
+  WebKit/gtk/webkit/ (e.g. by suppressing "readability/naming" errors
+  for files in this directory).
+
+  Consequently, if the path relative to the source root cannot be
+  determined for a file being checked, then style checking may not
+  work correctly for that file.  This can occur, for example, if no
+  WebKit checkout can be found, or if the source root can be detected,
+  but one of the files being checked lies outside the source tree.
+
+  If a WebKit checkout can be detected and all files being checked
+  are in the source tree, then all paths will automatically be
+  converted to paths relative to the source root prior to checking.
+  This is also useful for display purposes.
+
+  Currently, this command can detect the source root only if the
+  command is run from within a WebKit checkout (i.e. if the current
+  working directory is below the root of a checkout).  In particular,
+  it is not recommended to run this script from a directory outside
+  a checkout.
+
+  Running this script from a top-level WebKit source directory and
+  checking only files in the source tree will ensure that all style
+  checking behaves correctly -- whether or not a checkout can be
+  detected.  This is because all file paths will already be relative
+  to the source root and so will not need to be converted."""
+
+_EPILOG = ("This script can miss errors and does not substitute for "
+           "code review.")
+
+
+# This class should not have knowledge of the flag key names.
+class DefaultCommandOptionValues(object):
+
+    """Stores the default check-webkit-style command-line options.
+
+    Attributes:
+      output_format: A string that is the default output format.
+      min_confidence: An integer that is the default minimum confidence level.
+
+    """
+
+    def __init__(self, min_confidence, output_format):
+        self.min_confidence = min_confidence
+        self.output_format = output_format
+
+
+# This class should not have knowledge of the flag key names.
+class CommandOptionValues(object):
+
+    """Stores the option values passed by the user via the command line.
+
+    Attributes:
+      is_verbose: A boolean value of whether verbose logging is enabled.
+
+      filter_rules: The list of filter rules provided by the user.
+                    These rules are appended to the base rules and
+                    path-specific rules and so take precedence over
+                    the base filter rules, etc.
+
+      git_commit: A string representing the git commit to check.
+                  The default is None.
+
+      min_confidence: An integer between 1 and 5 inclusive that is the
+                      minimum confidence level of style errors to report.
+                      The default is 1, which reports all errors.
+
+      output_format: A string that is the output format.  The supported
+                     output formats are "emacs" which emacs can parse
+                     and "vs7" which Microsoft Visual Studio 7 can parse.
+
+    """
+    def __init__(self,
+                 filter_rules=None,
+                 git_commit=None,
+                 diff_files=None,
+                 is_verbose=False,
+                 min_confidence=1,
+                 output_format="emacs"):
+        if filter_rules is None:
+            filter_rules = []
+
+        if (min_confidence < 1) or (min_confidence > 5):
+            raise ValueError('Invalid "min_confidence" parameter: value '
+                             "must be an integer between 1 and 5 inclusive. "
+                             'Value given: "%s".' % min_confidence)
+
+        if output_format not in ("emacs", "vs7"):
+            raise ValueError('Invalid "output_format" parameter: '
+                             'value must be "emacs" or "vs7". '
+                             'Value given: "%s".' % output_format)
+
+        self.filter_rules = filter_rules
+        self.git_commit = git_commit
+        self.diff_files = diff_files
+        self.is_verbose = is_verbose
+        self.min_confidence = min_confidence
+        self.output_format = output_format
+
+    # Useful for unit testing.
+    def __eq__(self, other):
+        """Return whether this instance is equal to another."""
+        if self.filter_rules != other.filter_rules:
+            return False
+        if self.git_commit != other.git_commit:
+            return False
+        if self.diff_files != other.diff_files:
+            return False
+        if self.is_verbose != other.is_verbose:
+            return False
+        if self.min_confidence != other.min_confidence:
+            return False
+        if self.output_format != other.output_format:
+            return False
+
+        return True
+
+    # Useful for unit testing.
+    def __ne__(self, other):
+        # Python does not automatically deduce this from __eq__().
+        return not self.__eq__(other)
+
+
+class ArgumentPrinter(object):
+
+    """Supports the printing of check-webkit-style command arguments."""
+
+    def _flag_pair_to_string(self, flag_key, flag_value):
+        return '--%(key)s=%(val)s' % {'key': flag_key, 'val': flag_value }
+
+    def to_flag_string(self, options):
+        """Return a flag string of the given CommandOptionValues instance.
+
+        This method orders the flag values alphabetically by the flag key.
+
+        Args:
+          options: A CommandOptionValues instance.
+
+        """
+        flags = {}
+        flags['min-confidence'] = options.min_confidence
+        flags['output'] = options.output_format
+        # Only include the filter flag if user-provided rules are present.
+        filter_rules = options.filter_rules
+        if filter_rules:
+            flags['filter'] = ",".join(filter_rules)
+        if options.git_commit:
+            flags['git-commit'] = options.git_commit
+        if options.diff_files:
+            flags['diff_files'] = options.diff_files
+
+        flag_string = ''
+        # Alphabetizing lets us unit test this method.
+        for key in sorted(flags.keys()):
+            flag_string += self._flag_pair_to_string(key, flags[key]) + ' '
+
+        return flag_string.strip()
+
+
+class ArgumentParser(object):
+
+    # FIXME: Move the documentation of the attributes to the __init__
+    #        docstring after making the attributes internal.
+    """Supports the parsing of check-webkit-style command arguments.
+
+    Attributes:
+      create_usage: A function that accepts a DefaultCommandOptionValues
+                    instance and returns a string of usage instructions.
+                    Defaults to the function that generates the usage
+                    string for check-webkit-style.
+      default_options: A DefaultCommandOptionValues instance that provides
+                       the default values for options not explicitly
+                       provided by the user.
+      stderr_write: A function that takes a string as a parameter and
+                    serves as stderr.write.  Defaults to sys.stderr.write.
+                    This parameter should be specified only for unit tests.
+
+    """
+
+    def __init__(self,
+                 all_categories,
+                 default_options,
+                 base_filter_rules=None,
+                 mock_stderr=None,
+                 usage=None):
+        """Create an ArgumentParser instance.
+
+        Args:
+          all_categories: The set of all available style categories.
+          default_options: See the corresponding attribute in the class
+                           docstring.
+        Keyword Args:
+          base_filter_rules: The list of filter rules at the beginning of
+                             the list of rules used to check style.  This
+                             list has the least precedence when checking
+                             style and precedes any user-provided rules.
+                             The class uses this parameter only for display
+                             purposes to the user.  Defaults to the empty list.
+          create_usage: See the documentation of the corresponding
+                        attribute in the class docstring.
+          stderr_write: See the documentation of the corresponding
+                        attribute in the class docstring.
+
+        """
+        if base_filter_rules is None:
+            base_filter_rules = []
+        stderr = sys.stderr if mock_stderr is None else mock_stderr
+        if usage is None:
+            usage = _USAGE
+
+        self._all_categories = all_categories
+        self._base_filter_rules = base_filter_rules
+
+        # FIXME: Rename these to reflect that they are internal.
+        self.default_options = default_options
+        self.stderr_write = stderr.write
+
+        self._parser = self._create_option_parser(stderr=stderr,
+            usage=usage,
+            default_min_confidence=self.default_options.min_confidence,
+            default_output_format=self.default_options.output_format)
+
+    def _create_option_parser(self, stderr, usage,
+                              default_min_confidence, default_output_format):
+        # Since the epilog string is short, it is not necessary to replace
+        # the epilog string with a mock epilog string when testing.
+        # For this reason, we use _EPILOG directly rather than passing it
+        # as an argument like we do for the usage string.
+        parser = OptionParser(usage=usage, epilog=_EPILOG)
+
+        filter_help = ('set a filter to control what categories of style '
+                       'errors to report.  Specify a filter using a comma-'
+                       'delimited list of boolean filter rules, for example '
+                       '"--filter -whitespace,+whitespace/braces".  To display '
+                       'all categories and which are enabled by default, pass '
+                       """no value (e.g. '-f ""' or '--filter=').""")
+        parser.add_option("-f", "--filter-rules", metavar="RULES",
+                          dest="filter_value", help=filter_help)
+
+        git_commit_help = ("check all changes in the given commit. "
+                           "Use 'commit_id..' to check all changes after commmit_id")
+        parser.add_option("-g", "--git-diff", "--git-commit",
+                          metavar="COMMIT", dest="git_commit", help=git_commit_help,)
+
+        diff_files_help = "diff the files passed on the command line rather than checking the style of every line"
+        parser.add_option("--diff-files", action="store_true", dest="diff_files", default=False, help=diff_files_help)
+
+        min_confidence_help = ("set the minimum confidence of style errors "
+                               "to report.  Can be an integer 1-5, with 1 "
+                               "displaying all errors.  Defaults to %default.")
+        parser.add_option("-m", "--min-confidence", metavar="INT",
+                          type="int", dest="min_confidence",
+                          default=default_min_confidence,
+                          help=min_confidence_help)
+
+        output_format_help = ('set the output format, which can be "emacs" '
+                              'or "vs7" (for Visual Studio).  '
+                              'Defaults to "%default".')
+        parser.add_option("-o", "--output-format", metavar="FORMAT",
+                          choices=["emacs", "vs7"],
+                          dest="output_format", default=default_output_format,
+                          help=output_format_help)
+
+        verbose_help = "enable verbose logging."
+        parser.add_option("-v", "--verbose", dest="is_verbose", default=False,
+                          action="store_true", help=verbose_help)
+
+        # Override OptionParser's error() method so that option help will
+        # also display when an error occurs.  Normally, just the usage
+        # string displays and not option help.
+        parser.error = self._parse_error
+
+        # Override OptionParser's print_help() method so that help output
+        # does not render to the screen while running unit tests.
+        print_help = parser.print_help
+        parser.print_help = lambda: print_help(file=stderr)
+
+        return parser
+
+    def _parse_error(self, error_message):
+        """Print the help string and an error message, and exit."""
+        # The method format_help() includes both the usage string and
+        # the flag options.
+        help = self._parser.format_help()
+        # Separate help from the error message with a single blank line.
+        self.stderr_write(help + "\n")
+        if error_message:
+            _log.error(error_message)
+
+        # Since we are using this method to replace/override the Python
+        # module optparse's OptionParser.error() method, we match its
+        # behavior and exit with status code 2.
+        #
+        # As additional background, Python documentation says--
+        #
+        # "Unix programs generally use 2 for command line syntax errors
+        #  and 1 for all other kind of errors."
+        #
+        # (from http://docs.python.org/library/sys.html#sys.exit )
+        sys.exit(2)
+
+    def _exit_with_categories(self):
+        """Exit and print the style categories and default filter rules."""
+        self.stderr_write('\nAll categories:\n')
+        for category in sorted(self._all_categories):
+            self.stderr_write('    ' + category + '\n')
+
+        self.stderr_write('\nDefault filter rules**:\n')
+        for filter_rule in sorted(self._base_filter_rules):
+            self.stderr_write('    ' + filter_rule + '\n')
+        self.stderr_write('\n**The command always evaluates the above rules, '
+                          'and before any --filter flag.\n\n')
+
+        sys.exit(0)
+
+    def _parse_filter_flag(self, flag_value):
+        """Parse the --filter flag, and return a list of filter rules.
+
+        Args:
+          flag_value: A string of comma-separated filter rules, for
+                      example "-whitespace,+whitespace/indent".
+
+        """
+        filters = []
+        for uncleaned_filter in flag_value.split(','):
+            filter = uncleaned_filter.strip()
+            if not filter:
+                continue
+            filters.append(filter)
+        return filters
+
+    def parse(self, args):
+        """Parse the command line arguments to check-webkit-style.
+
+        Args:
+          args: A list of command-line arguments as returned by sys.argv[1:].
+
+        Returns:
+          A tuple of (paths, options)
+
+          paths: The list of paths to check.
+          options: A CommandOptionValues instance.
+
+        """
+        (options, paths) = self._parser.parse_args(args=args)
+
+        filter_value = options.filter_value
+        git_commit = options.git_commit
+        diff_files = options.diff_files
+        is_verbose = options.is_verbose
+        min_confidence = options.min_confidence
+        output_format = options.output_format
+
+        if filter_value is not None and not filter_value:
+            # Then the user explicitly passed no filter, for
+            # example "-f ''" or "--filter=".
+            self._exit_with_categories()
+
+        # Validate user-provided values.
+
+        min_confidence = int(min_confidence)
+        if (min_confidence < 1) or (min_confidence > 5):
+            self._parse_error('option --min-confidence: invalid integer: '
+                              '%s: value must be between 1 and 5'
+                              % min_confidence)
+
+        if filter_value:
+            filter_rules = self._parse_filter_flag(filter_value)
+        else:
+            filter_rules = []
+
+        try:
+            validate_filter_rules(filter_rules, self._all_categories)
+        except ValueError, err:
+            self._parse_error(err)
+
+        options = CommandOptionValues(filter_rules=filter_rules,
+                                      git_commit=git_commit,
+                                      diff_files=diff_files,
+                                      is_verbose=is_verbose,
+                                      min_confidence=min_confidence,
+                                      output_format=output_format)
+
+        return (paths, options)
+
diff --git a/Tools/Scripts/webkitpy/style/optparser_unittest.py b/Tools/Scripts/webkitpy/style/optparser_unittest.py
new file mode 100644
index 0000000..a6b64da
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/optparser_unittest.py
@@ -0,0 +1,258 @@
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+"""Unit tests for parser.py."""
+
+import unittest
+
+from webkitpy.common.system.logtesting import LoggingTestCase
+from webkitpy.style.optparser import ArgumentParser
+from webkitpy.style.optparser import ArgumentPrinter
+from webkitpy.style.optparser import CommandOptionValues as ProcessorOptions
+from webkitpy.style.optparser import DefaultCommandOptionValues
+
+
+class ArgumentPrinterTest(unittest.TestCase):
+
+    """Tests the ArgumentPrinter class."""
+
+    _printer = ArgumentPrinter()
+
+    def _create_options(self,
+                        output_format='emacs',
+                        min_confidence=3,
+                        filter_rules=[],
+                        git_commit=None):
+        return ProcessorOptions(filter_rules=filter_rules,
+                                git_commit=git_commit,
+                                min_confidence=min_confidence,
+                                output_format=output_format)
+
+    def test_to_flag_string(self):
+        options = self._create_options('vs7', 5, ['+foo', '-bar'], 'git')
+        self.assertEquals('--filter=+foo,-bar --git-commit=git '
+                          '--min-confidence=5 --output=vs7',
+                          self._printer.to_flag_string(options))
+
+        # This is to check that --filter and --git-commit do not
+        # show up when not user-specified.
+        options = self._create_options()
+        self.assertEquals('--min-confidence=3 --output=emacs',
+                          self._printer.to_flag_string(options))
+
+
+class ArgumentParserTest(LoggingTestCase):
+
+    """Test the ArgumentParser class."""
+
+    class _MockStdErr(object):
+
+        def write(self, message):
+            # We do not want the usage string or style categories
+            # to print during unit tests, so print nothing.
+            return
+
+    def _parse(self, args):
+        """Call a test parser.parse()."""
+        parser = self._create_parser()
+        return parser.parse(args)
+
+    def _create_defaults(self):
+        """Return a DefaultCommandOptionValues instance for testing."""
+        base_filter_rules = ["-", "+whitespace"]
+        return DefaultCommandOptionValues(min_confidence=3,
+                                          output_format="vs7")
+
+    def _create_parser(self):
+        """Return an ArgumentParser instance for testing."""
+        default_options = self._create_defaults()
+
+        all_categories = ["build" ,"whitespace"]
+
+        mock_stderr = self._MockStdErr()
+
+        return ArgumentParser(all_categories=all_categories,
+                              base_filter_rules=[],
+                              default_options=default_options,
+                              mock_stderr=mock_stderr,
+                              usage="test usage")
+
+    def test_parse_documentation(self):
+        parse = self._parse
+
+        # FIXME: Test both the printing of the usage string and the
+        #        filter categories help.
+
+        # Request the usage string.
+        self.assertRaises(SystemExit, parse, ['--help'])
+        # Request default filter rules and available style categories.
+        self.assertRaises(SystemExit, parse, ['--filter='])
+
+    def test_parse_bad_values(self):
+        parse = self._parse
+
+        # Pass an unsupported argument.
+        self.assertRaises(SystemExit, parse, ['--bad'])
+        self.assertLog(['ERROR: no such option: --bad\n'])
+
+        self.assertRaises(SystemExit, parse, ['--min-confidence=bad'])
+        self.assertLog(['ERROR: option --min-confidence: '
+                        "invalid integer value: 'bad'\n"])
+        self.assertRaises(SystemExit, parse, ['--min-confidence=0'])
+        self.assertLog(['ERROR: option --min-confidence: invalid integer: 0: '
+                        'value must be between 1 and 5\n'])
+        self.assertRaises(SystemExit, parse, ['--min-confidence=6'])
+        self.assertLog(['ERROR: option --min-confidence: invalid integer: 6: '
+                        'value must be between 1 and 5\n'])
+        parse(['--min-confidence=1']) # works
+        parse(['--min-confidence=5']) # works
+
+        self.assertRaises(SystemExit, parse, ['--output=bad'])
+        self.assertLog(['ERROR: option --output-format: invalid choice: '
+                        "'bad' (choose from 'emacs', 'vs7')\n"])
+        parse(['--output=vs7']) # works
+
+        # Pass a filter rule not beginning with + or -.
+        self.assertRaises(SystemExit, parse, ['--filter=build'])
+        self.assertLog(['ERROR: Invalid filter rule "build": '
+                        'every rule must start with + or -.\n'])
+        parse(['--filter=+build']) # works
+
+    def test_parse_default_arguments(self):
+        parse = self._parse
+
+        (files, options) = parse([])
+
+        self.assertEquals(files, [])
+
+        self.assertEquals(options.filter_rules, [])
+        self.assertEquals(options.git_commit, None)
+        self.assertEquals(options.diff_files, False)
+        self.assertEquals(options.is_verbose, False)
+        self.assertEquals(options.min_confidence, 3)
+        self.assertEquals(options.output_format, 'vs7')
+
+    def test_parse_explicit_arguments(self):
+        parse = self._parse
+
+        # Pass non-default explicit values.
+        (files, options) = parse(['--min-confidence=4'])
+        self.assertEquals(options.min_confidence, 4)
+        (files, options) = parse(['--output=emacs'])
+        self.assertEquals(options.output_format, 'emacs')
+        (files, options) = parse(['-g', 'commit'])
+        self.assertEquals(options.git_commit, 'commit')
+        (files, options) = parse(['--git-commit=commit'])
+        self.assertEquals(options.git_commit, 'commit')
+        (files, options) = parse(['--git-diff=commit'])
+        self.assertEquals(options.git_commit, 'commit')
+        (files, options) = parse(['--verbose'])
+        self.assertEquals(options.is_verbose, True)
+        (files, options) = parse(['--diff-files', 'file.txt'])
+        self.assertEquals(options.diff_files, True)
+
+        # Pass user_rules.
+        (files, options) = parse(['--filter=+build,-whitespace'])
+        self.assertEquals(options.filter_rules,
+                          ["+build", "-whitespace"])
+
+        # Pass spurious white space in user rules.
+        (files, options) = parse(['--filter=+build, -whitespace'])
+        self.assertEquals(options.filter_rules,
+                          ["+build", "-whitespace"])
+
+    def test_parse_files(self):
+        parse = self._parse
+
+        (files, options) = parse(['foo.cpp'])
+        self.assertEquals(files, ['foo.cpp'])
+
+        # Pass multiple files.
+        (files, options) = parse(['--output=emacs', 'foo.cpp', 'bar.cpp'])
+        self.assertEquals(files, ['foo.cpp', 'bar.cpp'])
+
+
+class CommandOptionValuesTest(unittest.TestCase):
+
+    """Tests CommandOptionValues class."""
+
+    def test_init(self):
+        """Test __init__ constructor."""
+        # Check default parameters.
+        options = ProcessorOptions()
+        self.assertEquals(options.filter_rules, [])
+        self.assertEquals(options.git_commit, None)
+        self.assertEquals(options.is_verbose, False)
+        self.assertEquals(options.min_confidence, 1)
+        self.assertEquals(options.output_format, "emacs")
+
+        # Check argument validation.
+        self.assertRaises(ValueError, ProcessorOptions, output_format="bad")
+        ProcessorOptions(output_format="emacs") # No ValueError: works
+        ProcessorOptions(output_format="vs7") # works
+        self.assertRaises(ValueError, ProcessorOptions, min_confidence=0)
+        self.assertRaises(ValueError, ProcessorOptions, min_confidence=6)
+        ProcessorOptions(min_confidence=1) # works
+        ProcessorOptions(min_confidence=5) # works
+
+        # Check attributes.
+        options = ProcessorOptions(filter_rules=["+"],
+                                   git_commit="commit",
+                                   is_verbose=True,
+                                   min_confidence=3,
+                                   output_format="vs7")
+        self.assertEquals(options.filter_rules, ["+"])
+        self.assertEquals(options.git_commit, "commit")
+        self.assertEquals(options.is_verbose, True)
+        self.assertEquals(options.min_confidence, 3)
+        self.assertEquals(options.output_format, "vs7")
+
+    def test_eq(self):
+        """Test __eq__ equality function."""
+        self.assertTrue(ProcessorOptions().__eq__(ProcessorOptions()))
+
+        # Also verify that a difference in any argument causes equality to fail.
+
+        # Explicitly create a ProcessorOptions instance with all default
+        # values.  We do this to be sure we are assuming the right default
+        # values in our self.assertFalse() calls below.
+        options = ProcessorOptions(filter_rules=[],
+                                   git_commit=None,
+                                   is_verbose=False,
+                                   min_confidence=1,
+                                   output_format="emacs")
+        # Verify that we created options correctly.
+        self.assertTrue(options.__eq__(ProcessorOptions()))
+
+        self.assertFalse(options.__eq__(ProcessorOptions(filter_rules=["+"])))
+        self.assertFalse(options.__eq__(ProcessorOptions(git_commit="commit")))
+        self.assertFalse(options.__eq__(ProcessorOptions(is_verbose=True)))
+        self.assertFalse(options.__eq__(ProcessorOptions(min_confidence=2)))
+        self.assertFalse(options.__eq__(ProcessorOptions(output_format="vs7")))
+
+    def test_ne(self):
+        """Test __ne__ inequality function."""
+        # By default, __ne__ always returns true on different objects.
+        # Thus, just check the distinguishing case to verify that the
+        # code defines __ne__.
+        self.assertFalse(ProcessorOptions().__ne__(ProcessorOptions()))
+
diff --git a/Tools/Scripts/webkitpy/style/patchreader.py b/Tools/Scripts/webkitpy/style/patchreader.py
new file mode 100644
index 0000000..8495cd0
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/patchreader.py
@@ -0,0 +1,83 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+# Copyright (C) 2010 ProFUSION embedded systems
+#
+# 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 logging
+import re
+
+from webkitpy.common.checkout.diff_parser import DiffParser
+from webkitpy.common.system.executive import Executive
+from webkitpy.common.system.filesystem import FileSystem
+from webkitpy.common.checkout.scm.detection import SCMDetector
+
+
+_log = logging.getLogger(__name__)
+
+
+class PatchReader(object):
+    """Supports checking style in patches."""
+
+    def __init__(self, text_file_reader):
+        """Create a PatchReader instance.
+
+        Args:
+          text_file_reader: A TextFileReader instance.
+
+        """
+        self._text_file_reader = text_file_reader
+
+    def check(self, patch_string, fs=None):
+        """Check style in the given patch."""
+        fs = fs or FileSystem()
+        patch_files = DiffParser(patch_string.splitlines()).files
+
+        # If the user uses git, checking subversion config file only once is enough.
+        call_only_once = True
+
+        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 not line_numbers:
+                match = re.search("\s*png$", path)
+                if match and fs.exists(path):
+                    if call_only_once:
+                        self._text_file_reader.process_file(file_path=path, line_numbers=None)
+                        cwd = FileSystem().getcwd()
+                        detection = SCMDetector(fs, Executive()).detect_scm_system(cwd)
+                        if detection.display_name() == "git":
+                            call_only_once = False
+                    continue
+                # 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/Tools/Scripts/webkitpy/style/patchreader_unittest.py b/Tools/Scripts/webkitpy/style/patchreader_unittest.py
new file mode 100644
index 0000000..eb26d47
--- /dev/null
+++ b/Tools/Scripts/webkitpy/style/patchreader_unittest.py
@@ -0,0 +1,103 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# Copyright (C) 2009 Torch Mobile Inc.
+# Copyright (C) 2009 Apple Inc. All rights reserved.
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+#
+# 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.system.filesystem_mock import MockFileSystem
+from webkitpy.style.patchreader import PatchReader
+
+
+class PatchReaderTest(unittest.TestCase):
+
+    """Test the PatchReader class."""
+
+    class MockTextFileReader(object):
+
+        def __init__(self):
+            self.passed_to_process_file = []
+            """A list of (file_path, line_numbers) pairs."""
+            self.delete_only_file_count = 0
+            """A number of times count_delete_only_file() called"""
+
+        def process_file(self, file_path, line_numbers):
+            self.passed_to_process_file.append((file_path, line_numbers))
+
+        def count_delete_only_file(self):
+            self.delete_only_file_count += 1
+
+    def setUp(self):
+        file_reader = self.MockTextFileReader()
+        self._file_reader = file_reader
+        self._patch_checker = PatchReader(file_reader)
+
+    def _call_check_patch(self, patch_string):
+        self._patch_checker.check(patch_string)
+
+    def _assert_checked(self, passed_to_process_file, delete_only_file_count):
+        self.assertEquals(self._file_reader.passed_to_process_file,
+                          passed_to_process_file)
+        self.assertEquals(self._file_reader.delete_only_file_count,
+                          delete_only_file_count)
+
+    def test_check_patch(self):
+        # The modified line_numbers array for this patch is: [2].
+        self._call_check_patch("""diff --git a/__init__.py b/__init__.py
+index ef65bee..e3db70e 100644
+--- a/__init__.py
++++ b/__init__.py
+@@ -1,1 +1,2 @@
+ # Required for Python to search this directory for module files
++# New line
+""")
+        self._assert_checked([("__init__.py", [2])], 0)
+
+    def test_check_patch_with_deletion(self):
+        self._call_check_patch("""Index: __init__.py
+===================================================================
+--- __init__.py  (revision 3593)
++++ __init__.py  (working copy)
+@@ -1 +0,0 @@
+-foobar
+""")
+        # _mock_check_file should not be called for the deletion patch.
+        self._assert_checked([], 1)
+
+    def test_check_patch_with_png_deletion(self):
+        fs = MockFileSystem()
+        diff_text = """Index: LayoutTests/platform/mac/foo-expected.png
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = image/png
+"""
+        self._patch_checker.check(diff_text, fs)
+        self._assert_checked([], 1)
diff --git a/Tools/Scripts/webkitpy/test/__init__.py b/Tools/Scripts/webkitpy/test/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/test/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/test/finder.py b/Tools/Scripts/webkitpy/test/finder.py
new file mode 100644
index 0000000..21eceac
--- /dev/null
+++ b/Tools/Scripts/webkitpy/test/finder.py
@@ -0,0 +1,173 @@
+# Copyright (C) 2012 Google, Inc.
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+"""this module is responsible for finding python tests."""
+
+import logging
+import re
+
+
+_log = logging.getLogger(__name__)
+
+
+class _DirectoryTree(object):
+    def __init__(self, filesystem, top_directory, starting_subdirectory):
+        self.filesystem = filesystem
+        self.top_directory = filesystem.realpath(top_directory)
+        self.search_directory = self.top_directory
+        self.top_package = ''
+        if starting_subdirectory:
+            self.top_package = starting_subdirectory.replace(filesystem.sep, '.') + '.'
+            self.search_directory = filesystem.join(self.top_directory, starting_subdirectory)
+
+    def find_modules(self, suffixes, sub_directory=None):
+        if sub_directory:
+            search_directory = self.filesystem.join(self.top_directory, sub_directory)
+        else:
+            search_directory = self.search_directory
+
+        def file_filter(filesystem, dirname, basename):
+            return any(basename.endswith(suffix) for suffix in suffixes)
+
+        filenames = self.filesystem.files_under(search_directory, file_filter=file_filter)
+        return [self.to_module(filename) for filename in filenames]
+
+    def to_module(self, path):
+        return path.replace(self.top_directory + self.filesystem.sep, '').replace(self.filesystem.sep, '.')[:-3]
+
+    def subpath(self, path):
+        """Returns the relative path from the top of the tree to the path, or None if the path is not under the top of the tree."""
+        realpath = self.filesystem.realpath(self.filesystem.join(self.top_directory, path))
+        if realpath.startswith(self.top_directory + self.filesystem.sep):
+            return realpath.replace(self.top_directory + self.filesystem.sep, '')
+        return None
+
+    def clean(self):
+        """Delete all .pyc files in the tree that have no matching .py file."""
+        _log.debug("Cleaning orphaned *.pyc files from: %s" % self.search_directory)
+        filenames = self.filesystem.files_under(self.search_directory)
+        for filename in filenames:
+            if filename.endswith(".pyc") and filename[:-1] not in filenames:
+                _log.info("Deleting orphan *.pyc file: %s" % filename)
+                self.filesystem.remove(filename)
+
+
+class Finder(object):
+    def __init__(self, filesystem):
+        self.filesystem = filesystem
+        self.trees = []
+        self._names_to_skip = []
+
+    def add_tree(self, top_directory, starting_subdirectory=None):
+        self.trees.append(_DirectoryTree(self.filesystem, top_directory, starting_subdirectory))
+
+    def skip(self, names, reason, bugid):
+        self._names_to_skip.append(tuple([names, reason, bugid]))
+
+    def additional_paths(self, paths):
+        return [tree.top_directory for tree in self.trees if tree.top_directory not in paths]
+
+    def clean_trees(self):
+        for tree in self.trees:
+            tree.clean()
+
+    def is_module(self, name):
+        relpath = name.replace('.', self.filesystem.sep) + '.py'
+        return any(self.filesystem.exists(self.filesystem.join(tree.top_directory, relpath)) for tree in self.trees)
+
+    def is_dotted_name(self, name):
+        return re.match(r'[a-zA-Z.][a-zA-Z0-9_.]*', name)
+
+    def to_module(self, path):
+        for tree in self.trees:
+            if path.startswith(tree.top_directory):
+                return tree.to_module(path)
+        return None
+
+    def find_names(self, args, find_all):
+        suffixes = ['_unittest.py', '_integrationtest.py']
+        if args:
+            names = []
+            for arg in args:
+                names.extend(self._find_names_for_arg(arg, suffixes))
+            return names
+
+        return self._default_names(suffixes, find_all)
+
+    def _find_names_for_arg(self, arg, suffixes):
+        realpath = self.filesystem.realpath(arg)
+        if self.filesystem.exists(realpath):
+            names = self._find_in_trees(realpath, suffixes)
+            if not names:
+                _log.error("%s is not in one of the test trees." % arg)
+            return names
+
+        # See if it's a python package in a tree (or a relative path from the top of a tree).
+        names = self._find_in_trees(arg.replace('.', self.filesystem.sep), suffixes)
+        if names:
+            return names
+
+        if self.is_dotted_name(arg):
+            # The name may not exist, but that's okay; we'll find out later.
+            return [arg]
+
+        _log.error("%s is not a python name or an existing file or directory." % arg)
+        return []
+
+    def _find_in_trees(self, path, suffixes):
+        for tree in self.trees:
+            relpath = tree.subpath(path)
+            if not relpath:
+                continue
+            if self.filesystem.isfile(path):
+                return [tree.to_module(path)]
+            else:
+                return tree.find_modules(suffixes, path)
+        return []
+
+    def _default_names(self, suffixes, find_all):
+        modules = []
+        for tree in self.trees:
+            modules.extend(tree.find_modules(suffixes))
+        modules.sort()
+
+        for module in modules:
+            _log.debug("Found: %s" % module)
+
+        if not find_all:
+            for (names, reason, bugid) in self._names_to_skip:
+                self._exclude(modules, names, reason, bugid)
+
+        return modules
+
+    def _exclude(self, modules, module_prefixes, reason, bugid):
+        _log.info('Skipping tests in the following modules or packages because they %s:' % reason)
+        for prefix in module_prefixes:
+            _log.info('    %s' % prefix)
+            modules_to_exclude = filter(lambda m: m.startswith(prefix), modules)
+            for m in modules_to_exclude:
+                if len(modules_to_exclude) > 1:
+                    _log.debug('        %s' % m)
+                modules.remove(m)
+        _log.info('    (https://bugs.webkit.org/show_bug.cgi?id=%d; use --all to include)' % bugid)
+        _log.info('')
diff --git a/Tools/Scripts/webkitpy/test/finder_unittest.py b/Tools/Scripts/webkitpy/test/finder_unittest.py
new file mode 100644
index 0000000..5c808a1
--- /dev/null
+++ b/Tools/Scripts/webkitpy/test/finder_unittest.py
@@ -0,0 +1,129 @@
+# Copyright (C) 2012 Google, Inc.
+#
+# 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.
+
+import logging
+import unittest
+
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.test.finder import Finder
+
+
+class FinderTest(unittest.TestCase):
+    def setUp(self):
+        files = {
+          '/foo/bar/baz.py': '',
+          '/foo/bar/baz_unittest.py': '',
+          '/foo2/bar2/baz2.py': '',
+          '/foo2/bar2/baz2.pyc': '',
+          '/foo2/bar2/baz2_integrationtest.py': '',
+          '/foo2/bar2/missing.pyc': '',
+          '/tmp/another_unittest.py': '',
+        }
+        self.fs = MockFileSystem(files)
+        self.finder = Finder(self.fs)
+        self.finder.add_tree('/foo', 'bar')
+        self.finder.add_tree('/foo2')
+
+        # Here we have to jump through a hoop to make sure test-webkitpy doesn't log
+        # any messages from these tests :(.
+        self.root_logger = logging.getLogger()
+        self.log_levels = []
+        self.log_handlers = self.root_logger.handlers[:]
+        for handler in self.log_handlers:
+            self.log_levels.append(handler.level)
+            handler.level = logging.CRITICAL
+
+    def tearDown(self):
+        for handler in self.log_handlers:
+            handler.level = self.log_levels.pop(0)
+
+    def test_additional_system_paths(self):
+        self.assertEquals(self.finder.additional_paths(['/usr']),
+                          ['/foo', '/foo2'])
+
+    def test_is_module(self):
+        self.assertTrue(self.finder.is_module('bar.baz'))
+        self.assertTrue(self.finder.is_module('bar2.baz2'))
+        self.assertTrue(self.finder.is_module('bar2.baz2_integrationtest'))
+
+        # Missing the proper namespace.
+        self.assertFalse(self.finder.is_module('baz'))
+
+    def test_to_module(self):
+        self.assertEquals(self.finder.to_module('/foo/test.py'), 'test')
+        self.assertEquals(self.finder.to_module('/foo/bar/test.py'), 'bar.test')
+        self.assertEquals(self.finder.to_module('/foo/bar/pytest.py'), 'bar.pytest')
+
+    def test_clean(self):
+        self.assertTrue(self.fs.exists('/foo2/bar2/missing.pyc'))
+        self.finder.clean_trees()
+        self.assertFalse(self.fs.exists('/foo2/bar2/missing.pyc'))
+
+    def check_names(self, names, expected_names, find_all=True):
+        self.assertEquals(self.finder.find_names(names, find_all), expected_names)
+
+    def test_default_names(self):
+        self.check_names([], ['bar.baz_unittest', 'bar2.baz2_integrationtest'], find_all=True)
+        self.check_names([], ['bar.baz_unittest', 'bar2.baz2_integrationtest'], find_all=False)
+
+        # Should return the names given it, even if they don't exist.
+        self.check_names(['foobar'], ['foobar'], find_all=False)
+
+    def test_paths(self):
+        self.fs.chdir('/foo/bar')
+        self.check_names(['baz_unittest.py'], ['bar.baz_unittest'])
+        self.check_names(['./baz_unittest.py'], ['bar.baz_unittest'])
+        self.check_names(['/foo/bar/baz_unittest.py'], ['bar.baz_unittest'])
+        self.check_names(['.'], ['bar.baz_unittest'])
+        self.check_names(['../../foo2/bar2'], ['bar2.baz2_integrationtest'])
+
+        self.fs.chdir('/')
+        self.check_names(['bar'], ['bar.baz_unittest'])
+        self.check_names(['/foo/bar/'], ['bar.baz_unittest'])
+
+        # This works 'by accident' since it maps onto a package.
+        self.check_names(['bar/'], ['bar.baz_unittest'])
+
+        # This should log an error, since it's outside the trees.
+        oc = OutputCapture()
+        oc.set_log_level(logging.ERROR)
+        oc.capture_output()
+        try:
+            self.check_names(['/tmp/another_unittest.py'], [])
+        finally:
+            _, _, logs = oc.restore_output()
+            self.assertTrue('another_unittest.py' in logs)
+
+        # Paths that don't exist are errors.
+        oc.capture_output()
+        try:
+            self.check_names(['/foo/bar/notexist_unittest.py'], [])
+        finally:
+            _, _, logs = oc.restore_output()
+            self.assertTrue('notexist_unittest.py' in logs)
+
+        # Names that don't exist are caught later, at load time.
+        self.check_names(['bar.notexist_unittest'], ['bar.notexist_unittest'])
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/test/main.py b/Tools/Scripts/webkitpy/test/main.py
new file mode 100644
index 0000000..e639a45
--- /dev/null
+++ b/Tools/Scripts/webkitpy/test/main.py
@@ -0,0 +1,234 @@
+# Copyright (C) 2012 Google, Inc.
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+"""unit testing code for webkitpy."""
+
+import logging
+import multiprocessing
+import optparse
+import os
+import StringIO
+import sys
+import time
+import traceback
+import unittest
+
+from webkitpy.common.system.filesystem import FileSystem
+from webkitpy.test.finder import Finder
+from webkitpy.test.printer import Printer
+from webkitpy.test.runner import Runner, unit_test_name
+
+_log = logging.getLogger(__name__)
+
+
+def main():
+    up = os.path.dirname
+    webkit_root = up(up(up(up(up(os.path.abspath(__file__))))))
+
+    tester = Tester()
+    tester.add_tree(os.path.join(webkit_root, 'Tools', 'Scripts'), 'webkitpy')
+    tester.add_tree(os.path.join(webkit_root, 'Source', 'WebKit2', 'Scripts'), 'webkit2')
+
+    tester.skip(('webkitpy.common.checkout.scm.scm_unittest',), 'are really, really, slow', 31818)
+    if sys.platform == 'win32':
+        tester.skip(('webkitpy.common.checkout', 'webkitpy.common.config', 'webkitpy.tool'), 'fail horribly on win32', 54526)
+
+    # This only needs to run on Unix, so don't worry about win32 for now.
+    appengine_sdk_path = '/usr/local/google_appengine'
+    if os.path.exists(appengine_sdk_path):
+        if not appengine_sdk_path in sys.path:
+            sys.path.append(appengine_sdk_path)
+        import dev_appserver
+        from google.appengine.dist import use_library
+        use_library('django', '1.2')
+        dev_appserver.fix_sys_path()
+        tester.add_tree(os.path.join(webkit_root, 'Tools', 'QueueStatusServer'))
+    else:
+        _log.info('Skipping QueueStatusServer tests; the Google AppEngine Python SDK is not installed.')
+
+    return not tester.run()
+
+
+class Tester(object):
+    def __init__(self, filesystem=None):
+        self.finder = Finder(filesystem or FileSystem())
+        self.printer = Printer(sys.stderr)
+        self._options = None
+
+    def add_tree(self, top_directory, starting_subdirectory=None):
+        self.finder.add_tree(top_directory, starting_subdirectory)
+
+    def skip(self, names, reason, bugid):
+        self.finder.skip(names, reason, bugid)
+
+    def _parse_args(self, argv=None):
+        parser = optparse.OptionParser(usage='usage: %prog [options] [args...]')
+        parser.add_option('-a', '--all', action='store_true', default=False,
+                          help='run all the tests')
+        parser.add_option('-c', '--coverage', action='store_true', default=False,
+                          help='generate code coverage info (requires http://pypi.python.org/pypi/coverage)')
+        parser.add_option('-i', '--integration-tests', action='store_true', default=False,
+                          help='run integration tests as well as unit tests'),
+        parser.add_option('-j', '--child-processes', action='store', type='int', default=(1 if sys.platform == 'win32' else multiprocessing.cpu_count()),
+                          help='number of tests to run in parallel (default=%default)')
+        parser.add_option('-p', '--pass-through', action='store_true', default=False,
+                          help='be debugger friendly by passing captured output through to the system')
+        parser.add_option('-q', '--quiet', action='store_true', default=False,
+                          help='run quietly (errors, warnings, and progress only)')
+        parser.add_option('-t', '--timing', action='store_true', default=False,
+                          help='display per-test execution time (implies --verbose)')
+        parser.add_option('-v', '--verbose', action='count', default=0,
+                          help='verbose output (specify once for individual test results, twice for debug messages)')
+
+        parser.epilog = ('[args...] is an optional list of modules, test_classes, or individual tests. '
+                         'If no args are given, all the tests will be run.')
+
+        return parser.parse_args(argv)
+
+    def run(self):
+        self._options, args = self._parse_args()
+        self.printer.configure(self._options)
+
+        self.finder.clean_trees()
+
+        names = self.finder.find_names(args, self._options.all)
+        if not names:
+            _log.error('No tests to run')
+            return False
+
+        return self._run_tests(names)
+
+    def _run_tests(self, names):
+        # Make sure PYTHONPATH is set up properly.
+        sys.path = self.finder.additional_paths(sys.path) + sys.path
+
+        # We autoinstall everything up so that we can run tests concurrently
+        # and not have to worry about autoinstalling packages concurrently.
+        self.printer.write_update("Checking autoinstalled packages ...")
+        from webkitpy.thirdparty import autoinstall_everything
+        installed_something = autoinstall_everything()
+
+        # FIXME: There appears to be a bug in Python 2.6.1 that is causing multiprocessing
+        # to hang after we install the packages in a clean checkout.
+        if installed_something:
+            _log.warning("We installed new packages, so running things serially at first")
+            self._options.child_processes = 1
+
+        if self._options.coverage:
+            import webkitpy.thirdparty.autoinstalled.coverage as coverage
+            cov = coverage.coverage()
+            cov.start()
+
+        self.printer.write_update("Checking imports ...")
+        if not self._check_imports(names):
+            return False
+
+        self.printer.write_update("Finding the individual test methods ...")
+        loader = _Loader()
+        parallel_tests, serial_tests = self._test_names(loader, names)
+
+        self.printer.write_update("Running the tests ...")
+        self.printer.num_tests = len(parallel_tests) + len(serial_tests)
+        start = time.time()
+        test_runner = Runner(self.printer, loader)
+        test_runner.run(parallel_tests, self._options.child_processes)
+        test_runner.run(serial_tests, 1)
+
+        self.printer.print_result(time.time() - start)
+
+        if self._options.coverage:
+            cov.stop()
+            cov.save()
+            cov.report(show_missing=False)
+
+        return not self.printer.num_errors and not self.printer.num_failures
+
+    def _check_imports(self, names):
+        for name in names:
+            if self.finder.is_module(name):
+                # if we failed to load a name and it looks like a module,
+                # try importing it directly, because loadTestsFromName()
+                # produces lousy error messages for bad modules.
+                try:
+                    __import__(name)
+                except ImportError:
+                    _log.fatal('Failed to import %s:' % name)
+                    self._log_exception()
+                    return False
+        return True
+
+    def _test_names(self, loader, names):
+        parallel_test_method_prefixes = ['test_']
+        serial_test_method_prefixes = ['serial_test_']
+        if self._options.integration_tests:
+            parallel_test_method_prefixes.append('integration_test_')
+            serial_test_method_prefixes.append('serial_integration_test_')
+
+        parallel_tests = []
+        loader.test_method_prefixes = parallel_test_method_prefixes
+        for name in names:
+            parallel_tests.extend(self._all_test_names(loader.loadTestsFromName(name, None)))
+
+        serial_tests = []
+        loader.test_method_prefixes = serial_test_method_prefixes
+        for name in names:
+            serial_tests.extend(self._all_test_names(loader.loadTestsFromName(name, None)))
+
+        # loader.loadTestsFromName() will not verify that names begin with one of the test_method_prefixes
+        # if the names were explicitly provided (e.g., MainTest.test_basic), so this means that any individual
+        # tests will be included in both parallel_tests and serial_tests, and we need to de-dup them.
+        serial_tests = list(set(serial_tests).difference(set(parallel_tests)))
+
+        return (parallel_tests, serial_tests)
+
+    def _all_test_names(self, suite):
+        names = []
+        if hasattr(suite, '_tests'):
+            for t in suite._tests:
+                names.extend(self._all_test_names(t))
+        else:
+            names.append(unit_test_name(suite))
+        return names
+
+    def _log_exception(self):
+        s = StringIO.StringIO()
+        traceback.print_exc(file=s)
+        for l in s.buflist:
+            _log.error('  ' + l.rstrip())
+
+
+class _Loader(unittest.TestLoader):
+    test_method_prefixes = []
+
+    def getTestCaseNames(self, testCaseClass):
+        def isTestMethod(attrname, testCaseClass=testCaseClass):
+            if not hasattr(getattr(testCaseClass, attrname), '__call__'):
+                return False
+            return (any(attrname.startswith(prefix) for prefix in self.test_method_prefixes))
+        testFnNames = filter(isTestMethod, dir(testCaseClass))
+        testFnNames.sort()
+        return testFnNames
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/Tools/Scripts/webkitpy/test/main_unittest.py b/Tools/Scripts/webkitpy/test/main_unittest.py
new file mode 100644
index 0000000..4fa6ef3
--- /dev/null
+++ b/Tools/Scripts/webkitpy/test/main_unittest.py
@@ -0,0 +1,117 @@
+# Copyright (C) 2012 Google, Inc.
+#
+# 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.
+
+import logging
+import sys
+import unittest
+import StringIO
+
+from webkitpy.common.system.filesystem import FileSystem
+from webkitpy.common.system.executive import Executive
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.test.main import Tester, _Loader
+
+
+STUBS_CLASS = __name__ + ".TestStubs"
+
+
+class TestStubs(unittest.TestCase):
+    def test_empty(self):
+        pass
+
+    def integration_test_empty(self):
+        pass
+
+    def serial_test_empty(self):
+        pass
+
+    def serial_integration_test_empty(self):
+        pass
+
+
+class TesterTest(unittest.TestCase):
+
+    def test_no_tests_found(self):
+        tester = Tester()
+        errors = StringIO.StringIO()
+
+        # Here we need to remove any existing log handlers so that they
+        # don't log the messages webkitpy.test while we're testing it.
+        root_logger = logging.getLogger()
+        root_handlers = root_logger.handlers
+        root_logger.handlers = []
+
+        tester.printer.stream = errors
+        tester.finder.find_names = lambda args, run_all: []
+        oc = OutputCapture()
+        try:
+            oc.capture_output()
+            self.assertFalse(tester.run())
+        finally:
+            _, _, logs = oc.restore_output()
+            root_logger.handlers = root_handlers
+
+        self.assertTrue('No tests to run' in errors.getvalue())
+        self.assertTrue('No tests to run' in logs)
+
+    def _find_test_names(self, args):
+        tester = Tester()
+        tester._options, args = tester._parse_args(args)
+        return tester._test_names(_Loader(), args)
+
+    def test_individual_names_are_not_run_twice(self):
+        args = [STUBS_CLASS + '.test_empty']
+        parallel_tests, serial_tests = self._find_test_names(args)
+        self.assertEquals(parallel_tests, args)
+        self.assertEquals(serial_tests, [])
+
+    def test_integration_tests_are_not_found_by_default(self):
+        parallel_tests, serial_tests = self._find_test_names([STUBS_CLASS])
+        self.assertEquals(parallel_tests, [
+            STUBS_CLASS + '.test_empty',
+            ])
+        self.assertEquals(serial_tests, [
+            STUBS_CLASS + '.serial_test_empty',
+            ])
+
+    def test_integration_tests_are_found(self):
+        parallel_tests, serial_tests = self._find_test_names(['--integration-tests', STUBS_CLASS])
+        self.assertEquals(parallel_tests, [
+            STUBS_CLASS + '.integration_test_empty',
+            STUBS_CLASS + '.test_empty',
+            ])
+        self.assertEquals(serial_tests, [
+            STUBS_CLASS + '.serial_integration_test_empty',
+            STUBS_CLASS + '.serial_test_empty',
+            ])
+
+    def integration_test_coverage_works(self):
+        filesystem = FileSystem()
+        executive = Executive()
+        module_path = filesystem.path_to_module(self.__module__)
+        script_dir = module_path[0:module_path.find('webkitpy') - 1]
+        proc = executive.popen([sys.executable, filesystem.join(script_dir, 'test-webkitpy'), '-c', STUBS_CLASS + '.test_empty'],
+                               stdout=executive.PIPE, stderr=executive.PIPE)
+        out, _ = proc.communicate()
+        retcode = proc.returncode
+        self.assertEquals(retcode, 0)
+        self.assertTrue('Cover' in out)
diff --git a/Tools/Scripts/webkitpy/test/printer.py b/Tools/Scripts/webkitpy/test/printer.py
new file mode 100644
index 0000000..0ec3035
--- /dev/null
+++ b/Tools/Scripts/webkitpy/test/printer.py
@@ -0,0 +1,199 @@
+# Copyright (C) 2012 Google, Inc.
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+import logging
+import StringIO
+
+from webkitpy.common.system import outputcapture
+from webkitpy.layout_tests.views.metered_stream import MeteredStream
+
+_log = logging.getLogger(__name__)
+
+
+class Printer(object):
+    def __init__(self, stream, options=None):
+        self.stream = stream
+        self.meter = None
+        self.options = options
+        self.num_tests = 0
+        self.num_completed = 0
+        self.num_errors = 0
+        self.num_failures = 0
+        self.running_tests = []
+        self.completed_tests = []
+        if options:
+            self.configure(options)
+
+    def configure(self, options):
+        self.options = options
+
+        if options.timing:
+            # --timing implies --verbose
+            options.verbose = max(options.verbose, 1)
+
+        log_level = logging.INFO
+        if options.quiet:
+            log_level = logging.WARNING
+        elif options.verbose == 2:
+            log_level = logging.DEBUG
+
+        self.meter = MeteredStream(self.stream, (options.verbose == 2))
+
+        handler = logging.StreamHandler(self.stream)
+        # We constrain the level on the handler rather than on the root
+        # logger itself.  This is probably better because the handler is
+        # configured and known only to this module, whereas the root logger
+        # is an object shared (and potentially modified) by many modules.
+        # Modifying the handler, then, is less intrusive and less likely to
+        # interfere with modifications made by other modules (e.g. in unit
+        # tests).
+        handler.name = __name__
+        handler.setLevel(log_level)
+        formatter = logging.Formatter("%(message)s")
+        handler.setFormatter(formatter)
+
+        logger = logging.getLogger()
+        logger.addHandler(handler)
+        logger.setLevel(logging.NOTSET)
+
+        # Filter out most webkitpy messages.
+        #
+        # Messages can be selectively re-enabled for this script by updating
+        # this method accordingly.
+        def filter_records(record):
+            """Filter out autoinstall and non-third-party webkitpy messages."""
+            # FIXME: Figure out a way not to use strings here, for example by
+            #        using syntax like webkitpy.test.__name__.  We want to be
+            #        sure not to import any non-Python 2.4 code, though, until
+            #        after the version-checking code has executed.
+            if (record.name.startswith("webkitpy.common.system.autoinstall") or
+                record.name.startswith("webkitpy.test")):
+                return True
+            if record.name.startswith("webkitpy"):
+                return False
+            return True
+
+        testing_filter = logging.Filter()
+        testing_filter.filter = filter_records
+
+        # Display a message so developers are not mystified as to why
+        # logging does not work in the unit tests.
+        _log.info("Suppressing most webkitpy logging while running unit tests.")
+        handler.addFilter(testing_filter)
+
+        if self.options.pass_through:
+            outputcapture.OutputCapture.stream_wrapper = _CaptureAndPassThroughStream
+
+    def write_update(self, msg):
+        self.meter.write_update(msg)
+
+    def print_started_test(self, source, test_name):
+        self.running_tests.append(test_name)
+        if len(self.running_tests) > 1:
+            suffix = ' (+%d)' % (len(self.running_tests) - 1)
+        else:
+            suffix = ''
+
+        if self.options.verbose:
+            write = self.meter.write_update
+        else:
+            write = self.meter.write_throttled_update
+
+        write(self._test_line(self.running_tests[0], suffix))
+
+    def print_finished_test(self, source, test_name, test_time, failures, errors):
+        write = self.meter.writeln
+        if failures:
+            lines = failures[0].splitlines() + ['']
+            suffix = ' failed:'
+            self.num_failures += 1
+        elif errors:
+            lines = errors[0].splitlines() + ['']
+            suffix = ' erred:'
+            self.num_errors += 1
+        else:
+            suffix = ' passed'
+            lines = []
+            if self.options.verbose:
+                write = self.meter.writeln
+            else:
+                write = self.meter.write_throttled_update
+        if self.options.timing:
+            suffix += ' %.4fs' % test_time
+
+        self.num_completed += 1
+
+        if test_name == self.running_tests[0]:
+            self.completed_tests.insert(0, [test_name, suffix, lines])
+        else:
+            self.completed_tests.append([test_name, suffix, lines])
+        self.running_tests.remove(test_name)
+
+        for test_name, msg, lines in self.completed_tests:
+            if lines:
+                self.meter.writeln(self._test_line(test_name, msg))
+                for line in lines:
+                    self.meter.writeln('  ' + line)
+            else:
+                write(self._test_line(test_name, msg))
+        self.completed_tests = []
+
+    def _test_line(self, test_name, suffix):
+        return '[%d/%d] %s%s' % (self.num_completed, self.num_tests, test_name, suffix)
+
+    def print_result(self, run_time):
+        write = self.meter.writeln
+        write('Ran %d test%s in %.3fs' % (self.num_completed, self.num_completed != 1 and "s" or "", run_time))
+        if self.num_failures or self.num_errors:
+            write('FAILED (failures=%d, errors=%d)\n' % (self.num_failures, self.num_errors))
+        else:
+            write('\nOK\n')
+
+
+class _CaptureAndPassThroughStream(object):
+    def __init__(self, stream):
+        self._buffer = StringIO.StringIO()
+        self._stream = stream
+
+    def write(self, msg):
+        self._stream.write(msg)
+
+        # Note that we don't want to capture any output generated by the debugger
+        # because that could cause the results of capture_output() to be invalid.
+        if not self._message_is_from_pdb():
+            self._buffer.write(msg)
+
+    def _message_is_from_pdb(self):
+        # We will assume that if the pdb module is in the stack then the output
+        # is being generated by the python debugger (or the user calling something
+        # from inside the debugger).
+        import inspect
+        import pdb
+        stack = inspect.stack()
+        return any(frame[1] == pdb.__file__.replace('.pyc', '.py') for frame in stack)
+
+    def flush(self):
+        self._stream.flush()
+
+    def getvalue(self):
+        return self._buffer.getvalue()
diff --git a/Tools/Scripts/webkitpy/test/runner.py b/Tools/Scripts/webkitpy/test/runner.py
new file mode 100644
index 0000000..d3f5764
--- /dev/null
+++ b/Tools/Scripts/webkitpy/test/runner.py
@@ -0,0 +1,82 @@
+# Copyright (C) 2012 Google, Inc.
+#
+# 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.
+
+"""code to actually run a list of python tests."""
+
+import re
+import time
+import unittest
+
+from webkitpy.common import message_pool
+
+_test_description = re.compile("(\w+) \(([\w.]+)\)")
+
+
+def unit_test_name(test):
+    m = _test_description.match(str(test))
+    return "%s.%s" % (m.group(2), m.group(1))
+
+
+class Runner(object):
+    def __init__(self, printer, loader):
+        self.printer = printer
+        self.loader = loader
+        self.tests_run = 0
+        self.errors = []
+        self.failures = []
+        self.worker_factory = lambda caller: _Worker(caller, self.loader)
+
+    def run(self, test_names, num_workers):
+        if not test_names:
+            return
+        num_workers = min(num_workers, len(test_names))
+        with message_pool.get(self, self.worker_factory, num_workers) as pool:
+            pool.run(('test', test_name) for test_name in test_names)
+
+    def handle(self, message_name, source, test_name, delay=None, failures=None, errors=None):
+        if message_name == 'started_test':
+            self.printer.print_started_test(source, test_name)
+            return
+
+        self.tests_run += 1
+        if failures:
+            self.failures.append((test_name, failures))
+        if errors:
+            self.errors.append((test_name, errors))
+        self.printer.print_finished_test(source, test_name, delay, failures, errors)
+
+
+class _Worker(object):
+    def __init__(self, caller, loader):
+        self._caller = caller
+        self._loader = loader
+
+    def handle(self, message_name, source, test_name):
+        assert message_name == 'test'
+        result = unittest.TestResult()
+        start = time.time()
+        self._caller.post('started_test', test_name)
+
+        # We will need to rework this if a test_name results in multiple tests.
+        self._loader.loadTestsFromName(test_name, None).run(result)
+        self._caller.post('finished_test', test_name, time.time() - start,
+            [failure[1] for failure in result.failures], [error[1] for error in result.errors])
diff --git a/Tools/Scripts/webkitpy/test/runner_unittest.py b/Tools/Scripts/webkitpy/test/runner_unittest.py
new file mode 100644
index 0000000..8fe1b06
--- /dev/null
+++ b/Tools/Scripts/webkitpy/test/runner_unittest.py
@@ -0,0 +1,101 @@
+# Copyright (C) 2012 Google, Inc.
+#
+# 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.
+
+import logging
+import re
+import StringIO
+import unittest
+
+from webkitpy.tool.mocktool import MockOptions
+from webkitpy.test.printer import Printer
+from webkitpy.test.runner import Runner
+
+
+class FakeModuleSuite(object):
+    def __init__(self, name, result, msg):
+        self.name = name
+        self.result = result
+        self.msg = msg
+
+    def __str__(self):
+        return self.name
+
+    def run(self, result):
+        result.testsRun += 1
+        if self.result == 'F':
+            result.failures.append((self.name, self.msg))
+        elif self.result == 'E':
+            result.errors.append((self.name, self.msg))
+
+
+class FakeTopSuite(object):
+    def __init__(self, tests):
+        self._tests = tests
+
+
+class FakeLoader(object):
+    def __init__(self, *test_triples):
+        self.triples = test_triples
+        self._tests = []
+        self._results = {}
+        for test_name, result, msg in self.triples:
+            self._tests.append(test_name)
+            m = re.match("(\w+) \(([\w.]+)\)", test_name)
+            self._results['%s.%s' % (m.group(2), m.group(1))] = tuple([test_name, result, msg])
+
+    def top_suite(self):
+        return FakeTopSuite(self._tests)
+
+    def loadTestsFromName(self, name, _):
+        return FakeModuleSuite(*self._results[name])
+
+
+class RunnerTest(unittest.TestCase):
+    def setUp(self):
+        # Here we have to jump through a hoop to make sure test-webkitpy doesn't log
+        # any messages from these tests :(.
+        self.root_logger = logging.getLogger()
+        self.log_levels = []
+        self.log_handlers = self.root_logger.handlers[:]
+        for handler in self.log_handlers:
+            self.log_levels.append(handler.level)
+            handler.level = logging.CRITICAL
+
+    def tearDown(self):
+        for handler in self.log_handlers:
+            handler.level = self.log_levels.pop(0)
+
+    def test_run(self, verbose=0, timing=False, child_processes=1, quiet=False):
+        options = MockOptions(verbose=verbose, timing=timing, child_processes=child_processes, quiet=quiet, pass_through=False)
+        stream = StringIO.StringIO()
+        loader = FakeLoader(('test1 (Foo)', '.', ''),
+                            ('test2 (Foo)', 'F', 'test2\nfailed'),
+                            ('test3 (Foo)', 'E', 'test3\nerred'))
+        runner = Runner(Printer(stream, options), loader)
+        runner.run(['Foo.test1', 'Foo.test2', 'Foo.test3'], 1)
+        self.assertEquals(runner.tests_run, 3)
+        self.assertEquals(len(runner.failures), 1)
+        self.assertEquals(len(runner.errors), 1)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/test/skip.py b/Tools/Scripts/webkitpy/test/skip.py
new file mode 100644
index 0000000..8587d56
--- /dev/null
+++ b/Tools/Scripts/webkitpy/test/skip.py
@@ -0,0 +1,52 @@
+# 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.
+
+import logging
+
+_log = logging.getLogger(__name__)
+
+
+def skip_if(klass, condition, message=None, logger=None):
+    """Makes all test_* methods in a given class no-ops if the given condition
+    is False. Backported from Python 3.1+'s unittest.skipIf decorator."""
+    if not logger:
+        logger = _log
+    if not condition:
+        return klass
+    for name in dir(klass):
+        attr = getattr(klass, name)
+        if not callable(attr):
+            continue
+        if not name.startswith('test_'):
+            continue
+        setattr(klass, name, _skipped_method(attr, message, logger))
+    klass._printed_skipped_message = False
+    return klass
+
+
+def _skipped_method(method, message, logger):
+    def _skip(*args):
+        if method.im_class._printed_skipped_message:
+            return
+        method.im_class._printed_skipped_message = True
+        logger.info('Skipping %s.%s: %s' % (method.__module__, method.im_class.__name__, message))
+    return _skip
diff --git a/Tools/Scripts/webkitpy/test/skip_unittest.py b/Tools/Scripts/webkitpy/test/skip_unittest.py
new file mode 100644
index 0000000..f61a1bb
--- /dev/null
+++ b/Tools/Scripts/webkitpy/test/skip_unittest.py
@@ -0,0 +1,77 @@
+# 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.
+
+import StringIO
+import logging
+import unittest
+
+from webkitpy.test.skip import skip_if
+
+
+class SkipTest(unittest.TestCase):
+    def setUp(self):
+        self.logger = logging.getLogger(__name__)
+
+        self.old_level = self.logger.level
+        self.logger.setLevel(logging.INFO)
+
+        self.old_propagate = self.logger.propagate
+        self.logger.propagate = False
+
+        self.log_stream = StringIO.StringIO()
+        self.handler = logging.StreamHandler(self.log_stream)
+        self.logger.addHandler(self.handler)
+
+        self.foo_was_called = False
+
+    def tearDown(self):
+        self.logger.removeHandler(self.handler)
+        self.propagate = self.old_propagate
+        self.logger.setLevel(self.old_level)
+
+    def create_fixture_class(self):
+        class TestSkipFixture(object):
+            def __init__(self, callback):
+                self.callback = callback
+
+            def test_foo(self):
+                self.callback()
+
+        return TestSkipFixture
+
+    def foo_callback(self):
+        self.foo_was_called = True
+
+    def test_skip_if_false(self):
+        klass = skip_if(self.create_fixture_class(), False, 'Should not see this message.', logger=self.logger)
+        klass(self.foo_callback).test_foo()
+        self.assertEqual(self.log_stream.getvalue(), '')
+        self.assertTrue(self.foo_was_called)
+
+    def test_skip_if_true(self):
+        klass = skip_if(self.create_fixture_class(), True, 'Should see this message.', logger=self.logger)
+        klass(self.foo_callback).test_foo()
+        self.assertEqual(self.log_stream.getvalue(), 'Skipping webkitpy.test.skip_unittest.TestSkipFixture: Should see this message.\n')
+        self.assertFalse(self.foo_was_called)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/thirdparty/BeautifulSoup.py b/Tools/Scripts/webkitpy/thirdparty/BeautifulSoup.py
new file mode 100644
index 0000000..4b17b85
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/BeautifulSoup.py
@@ -0,0 +1,2014 @@
+"""Beautiful Soup
+Elixir and Tonic
+"The Screen-Scraper's Friend"
+http://www.crummy.com/software/BeautifulSoup/
+
+Beautiful Soup parses a (possibly invalid) XML or HTML document into a
+tree representation. It provides methods and Pythonic idioms that make
+it easy to navigate, search, and modify the tree.
+
+A well-formed XML/HTML document yields a well-formed data
+structure. An ill-formed XML/HTML document yields a correspondingly
+ill-formed data structure. If your document is only locally
+well-formed, you can use this library to find and process the
+well-formed part of it.
+
+Beautiful Soup works with Python 2.2 and up. It has no external
+dependencies, but you'll have more success at converting data to UTF-8
+if you also install these three packages:
+
+* chardet, for auto-detecting character encodings
+  http://chardet.feedparser.org/
+* cjkcodecs and iconv_codec, which add more encodings to the ones supported
+  by stock Python.
+  http://cjkpython.i18n.org/
+
+Beautiful Soup defines classes for two main parsing strategies:
+
+ * BeautifulStoneSoup, for parsing XML, SGML, or your domain-specific
+   language that kind of looks like XML.
+
+ * BeautifulSoup, for parsing run-of-the-mill HTML code, be it valid
+   or invalid. This class has web browser-like heuristics for
+   obtaining a sensible parse tree in the face of common HTML errors.
+
+Beautiful Soup also defines a class (UnicodeDammit) for autodetecting
+the encoding of an HTML or XML document, and converting it to
+Unicode. Much of this code is taken from Mark Pilgrim's Universal Feed Parser.
+
+For more than you ever wanted to know about Beautiful Soup, see the
+documentation:
+http://www.crummy.com/software/BeautifulSoup/documentation.html
+
+Here, have some legalese:
+
+Copyright (c) 2004-2010, Leonard Richardson
+
+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 the the Beautiful Soup Consortium and All
+    Night Kosher Bakery 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, DAMMIT.
+
+"""
+from __future__ import generators
+
+__author__ = "Leonard Richardson (leonardr@segfault.org)"
+__version__ = "3.2.0"
+__copyright__ = "Copyright (c) 2004-2010 Leonard Richardson"
+__license__ = "New-style BSD"
+
+from sgmllib import SGMLParser, SGMLParseError
+import codecs
+import markupbase
+import types
+import re
+import sgmllib
+try:
+  from htmlentitydefs import name2codepoint
+except ImportError:
+  name2codepoint = {}
+try:
+    set
+except NameError:
+    from sets import Set as set
+
+#These hacks make Beautiful Soup able to parse XML with namespaces
+sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*')
+markupbase._declname_match = re.compile(r'[a-zA-Z][-_.:a-zA-Z0-9]*\s*').match
+
+DEFAULT_OUTPUT_ENCODING = "utf-8"
+
+def _match_css_class(str):
+    """Build a RE to match the given CSS class."""
+    return re.compile(r"(^|.*\s)%s($|\s)" % str)
+
+# First, the classes that represent markup elements.
+
+class PageElement(object):
+    """Contains the navigational information for some part of the page
+    (either a tag or a piece of text)"""
+
+    def setup(self, parent=None, previous=None):
+        """Sets up the initial relations between this element and
+        other elements."""
+        self.parent = parent
+        self.previous = previous
+        self.next = None
+        self.previousSibling = None
+        self.nextSibling = None
+        if self.parent and self.parent.contents:
+            self.previousSibling = self.parent.contents[-1]
+            self.previousSibling.nextSibling = self
+
+    def replaceWith(self, replaceWith):
+        oldParent = self.parent
+        myIndex = self.parent.index(self)
+        if hasattr(replaceWith, "parent")\
+                  and replaceWith.parent is self.parent:
+            # We're replacing this element with one of its siblings.
+            index = replaceWith.parent.index(replaceWith)
+            if index and index < myIndex:
+                # Furthermore, it comes before this element. That
+                # means that when we extract it, the index of this
+                # element will change.
+                myIndex = myIndex - 1
+        self.extract()
+        oldParent.insert(myIndex, replaceWith)
+
+    def replaceWithChildren(self):
+        myParent = self.parent
+        myIndex = self.parent.index(self)
+        self.extract()
+        reversedChildren = list(self.contents)
+        reversedChildren.reverse()
+        for child in reversedChildren:
+            myParent.insert(myIndex, child)
+
+    def extract(self):
+        """Destructively rips this element out of the tree."""
+        if self.parent:
+            try:
+                del self.parent.contents[self.parent.index(self)]
+            except ValueError:
+                pass
+
+        #Find the two elements that would be next to each other if
+        #this element (and any children) hadn't been parsed. Connect
+        #the two.
+        lastChild = self._lastRecursiveChild()
+        nextElement = lastChild.next
+
+        if self.previous:
+            self.previous.next = nextElement
+        if nextElement:
+            nextElement.previous = self.previous
+        self.previous = None
+        lastChild.next = None
+
+        self.parent = None
+        if self.previousSibling:
+            self.previousSibling.nextSibling = self.nextSibling
+        if self.nextSibling:
+            self.nextSibling.previousSibling = self.previousSibling
+        self.previousSibling = self.nextSibling = None
+        return self
+
+    def _lastRecursiveChild(self):
+        "Finds the last element beneath this object to be parsed."
+        lastChild = self
+        while hasattr(lastChild, 'contents') and lastChild.contents:
+            lastChild = lastChild.contents[-1]
+        return lastChild
+
+    def insert(self, position, newChild):
+        if isinstance(newChild, basestring) \
+            and not isinstance(newChild, NavigableString):
+            newChild = NavigableString(newChild)
+
+        position =  min(position, len(self.contents))
+        if hasattr(newChild, 'parent') and newChild.parent is not None:
+            # We're 'inserting' an element that's already one
+            # of this object's children.
+            if newChild.parent is self:
+                index = self.index(newChild)
+                if index > position:
+                    # Furthermore we're moving it further down the
+                    # list of this object's children. That means that
+                    # when we extract this element, our target index
+                    # will jump down one.
+                    position = position - 1
+            newChild.extract()
+
+        newChild.parent = self
+        previousChild = None
+        if position == 0:
+            newChild.previousSibling = None
+            newChild.previous = self
+        else:
+            previousChild = self.contents[position-1]
+            newChild.previousSibling = previousChild
+            newChild.previousSibling.nextSibling = newChild
+            newChild.previous = previousChild._lastRecursiveChild()
+        if newChild.previous:
+            newChild.previous.next = newChild
+
+        newChildsLastElement = newChild._lastRecursiveChild()
+
+        if position >= len(self.contents):
+            newChild.nextSibling = None
+
+            parent = self
+            parentsNextSibling = None
+            while not parentsNextSibling:
+                parentsNextSibling = parent.nextSibling
+                parent = parent.parent
+                if not parent: # This is the last element in the document.
+                    break
+            if parentsNextSibling:
+                newChildsLastElement.next = parentsNextSibling
+            else:
+                newChildsLastElement.next = None
+        else:
+            nextChild = self.contents[position]
+            newChild.nextSibling = nextChild
+            if newChild.nextSibling:
+                newChild.nextSibling.previousSibling = newChild
+            newChildsLastElement.next = nextChild
+
+        if newChildsLastElement.next:
+            newChildsLastElement.next.previous = newChildsLastElement
+        self.contents.insert(position, newChild)
+
+    def append(self, tag):
+        """Appends the given tag to the contents of this tag."""
+        self.insert(len(self.contents), tag)
+
+    def findNext(self, name=None, attrs={}, text=None, **kwargs):
+        """Returns the first item that matches the given criteria and
+        appears after this Tag in the document."""
+        return self._findOne(self.findAllNext, name, attrs, text, **kwargs)
+
+    def findAllNext(self, name=None, attrs={}, text=None, limit=None,
+                    **kwargs):
+        """Returns all items that match the given criteria and appear
+        after this Tag in the document."""
+        return self._findAll(name, attrs, text, limit, self.nextGenerator,
+                             **kwargs)
+
+    def findNextSibling(self, name=None, attrs={}, text=None, **kwargs):
+        """Returns the closest sibling to this Tag that matches the
+        given criteria and appears after this Tag in the document."""
+        return self._findOne(self.findNextSiblings, name, attrs, text,
+                             **kwargs)
+
+    def findNextSiblings(self, name=None, attrs={}, text=None, limit=None,
+                         **kwargs):
+        """Returns the siblings of this Tag that match the given
+        criteria and appear after this Tag in the document."""
+        return self._findAll(name, attrs, text, limit,
+                             self.nextSiblingGenerator, **kwargs)
+    fetchNextSiblings = findNextSiblings # Compatibility with pre-3.x
+
+    def findPrevious(self, name=None, attrs={}, text=None, **kwargs):
+        """Returns the first item that matches the given criteria and
+        appears before this Tag in the document."""
+        return self._findOne(self.findAllPrevious, name, attrs, text, **kwargs)
+
+    def findAllPrevious(self, name=None, attrs={}, text=None, limit=None,
+                        **kwargs):
+        """Returns all items that match the given criteria and appear
+        before this Tag in the document."""
+        return self._findAll(name, attrs, text, limit, self.previousGenerator,
+                           **kwargs)
+    fetchPrevious = findAllPrevious # Compatibility with pre-3.x
+
+    def findPreviousSibling(self, name=None, attrs={}, text=None, **kwargs):
+        """Returns the closest sibling to this Tag that matches the
+        given criteria and appears before this Tag in the document."""
+        return self._findOne(self.findPreviousSiblings, name, attrs, text,
+                             **kwargs)
+
+    def findPreviousSiblings(self, name=None, attrs={}, text=None,
+                             limit=None, **kwargs):
+        """Returns the siblings of this Tag that match the given
+        criteria and appear before this Tag in the document."""
+        return self._findAll(name, attrs, text, limit,
+                             self.previousSiblingGenerator, **kwargs)
+    fetchPreviousSiblings = findPreviousSiblings # Compatibility with pre-3.x
+
+    def findParent(self, name=None, attrs={}, **kwargs):
+        """Returns the closest parent of this Tag that matches the given
+        criteria."""
+        # NOTE: We can't use _findOne because findParents takes a different
+        # set of arguments.
+        r = None
+        l = self.findParents(name, attrs, 1)
+        if l:
+            r = l[0]
+        return r
+
+    def findParents(self, name=None, attrs={}, limit=None, **kwargs):
+        """Returns the parents of this Tag that match the given
+        criteria."""
+
+        return self._findAll(name, attrs, None, limit, self.parentGenerator,
+                             **kwargs)
+    fetchParents = findParents # Compatibility with pre-3.x
+
+    #These methods do the real heavy lifting.
+
+    def _findOne(self, method, name, attrs, text, **kwargs):
+        r = None
+        l = method(name, attrs, text, 1, **kwargs)
+        if l:
+            r = l[0]
+        return r
+
+    def _findAll(self, name, attrs, text, limit, generator, **kwargs):
+        "Iterates over a generator looking for things that match."
+
+        if isinstance(name, SoupStrainer):
+            strainer = name
+        # (Possibly) special case some findAll*(...) searches
+        elif text is None and not limit and not attrs and not kwargs:
+            # findAll*(True)
+            if name is True:
+                return [element for element in generator()
+                        if isinstance(element, Tag)]
+            # findAll*('tag-name')
+            elif isinstance(name, basestring):
+                return [element for element in generator()
+                        if isinstance(element, Tag) and
+                        element.name == name]
+            else:
+                strainer = SoupStrainer(name, attrs, text, **kwargs)
+        # Build a SoupStrainer
+        else:
+            strainer = SoupStrainer(name, attrs, text, **kwargs)
+        results = ResultSet(strainer)
+        g = generator()
+        while True:
+            try:
+                i = g.next()
+            except StopIteration:
+                break
+            if i:
+                found = strainer.search(i)
+                if found:
+                    results.append(found)
+                    if limit and len(results) >= limit:
+                        break
+        return results
+
+    #These Generators can be used to navigate starting from both
+    #NavigableStrings and Tags.
+    def nextGenerator(self):
+        i = self
+        while i is not None:
+            i = i.next
+            yield i
+
+    def nextSiblingGenerator(self):
+        i = self
+        while i is not None:
+            i = i.nextSibling
+            yield i
+
+    def previousGenerator(self):
+        i = self
+        while i is not None:
+            i = i.previous
+            yield i
+
+    def previousSiblingGenerator(self):
+        i = self
+        while i is not None:
+            i = i.previousSibling
+            yield i
+
+    def parentGenerator(self):
+        i = self
+        while i is not None:
+            i = i.parent
+            yield i
+
+    # Utility methods
+    def substituteEncoding(self, str, encoding=None):
+        encoding = encoding or "utf-8"
+        return str.replace("%SOUP-ENCODING%", encoding)
+
+    def toEncoding(self, s, encoding=None):
+        """Encodes an object to a string in some encoding, or to Unicode.
+        ."""
+        if isinstance(s, unicode):
+            if encoding:
+                s = s.encode(encoding)
+        elif isinstance(s, str):
+            if encoding:
+                s = s.encode(encoding)
+            else:
+                s = unicode(s)
+        else:
+            if encoding:
+                s  = self.toEncoding(str(s), encoding)
+            else:
+                s = unicode(s)
+        return s
+
+class NavigableString(unicode, PageElement):
+
+    def __new__(cls, value):
+        """Create a new NavigableString.
+
+        When unpickling a NavigableString, this method is called with
+        the string in DEFAULT_OUTPUT_ENCODING. That encoding needs to be
+        passed in to the superclass's __new__ or the superclass won't know
+        how to handle non-ASCII characters.
+        """
+        if isinstance(value, unicode):
+            return unicode.__new__(cls, value)
+        return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING)
+
+    def __getnewargs__(self):
+        return (NavigableString.__str__(self),)
+
+    def __getattr__(self, attr):
+        """text.string gives you text. This is for backwards
+        compatibility for Navigable*String, but for CData* it lets you
+        get the string without the CData wrapper."""
+        if attr == 'string':
+            return self
+        else:
+            raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__.__name__, attr)
+
+    def __unicode__(self):
+        return str(self).decode(DEFAULT_OUTPUT_ENCODING)
+
+    def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
+        if encoding:
+            return self.encode(encoding)
+        else:
+            return self
+
+class CData(NavigableString):
+
+    def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
+        return "<![CDATA[%s]]>" % NavigableString.__str__(self, encoding)
+
+class ProcessingInstruction(NavigableString):
+    def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
+        output = self
+        if "%SOUP-ENCODING%" in output:
+            output = self.substituteEncoding(output, encoding)
+        return "<?%s?>" % self.toEncoding(output, encoding)
+
+class Comment(NavigableString):
+    def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
+        return "<!--%s-->" % NavigableString.__str__(self, encoding)
+
+class Declaration(NavigableString):
+    def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
+        return "<!%s>" % NavigableString.__str__(self, encoding)
+
+class Tag(PageElement):
+
+    """Represents a found HTML tag with its attributes and contents."""
+
+    def _invert(h):
+        "Cheap function to invert a hash."
+        i = {}
+        for k,v in h.items():
+            i[v] = k
+        return i
+
+    XML_ENTITIES_TO_SPECIAL_CHARS = { "apos" : "'",
+                                      "quot" : '"',
+                                      "amp" : "&",
+                                      "lt" : "<",
+                                      "gt" : ">" }
+
+    XML_SPECIAL_CHARS_TO_ENTITIES = _invert(XML_ENTITIES_TO_SPECIAL_CHARS)
+
+    def _convertEntities(self, match):
+        """Used in a call to re.sub to replace HTML, XML, and numeric
+        entities with the appropriate Unicode characters. If HTML
+        entities are being converted, any unrecognized entities are
+        escaped."""
+        x = match.group(1)
+        if self.convertHTMLEntities and x in name2codepoint:
+            return unichr(name2codepoint[x])
+        elif x in self.XML_ENTITIES_TO_SPECIAL_CHARS:
+            if self.convertXMLEntities:
+                return self.XML_ENTITIES_TO_SPECIAL_CHARS[x]
+            else:
+                return u'&%s;' % x
+        elif len(x) > 0 and x[0] == '#':
+            # Handle numeric entities
+            if len(x) > 1 and x[1] == 'x':
+                return unichr(int(x[2:], 16))
+            else:
+                return unichr(int(x[1:]))
+
+        elif self.escapeUnrecognizedEntities:
+            return u'&amp;%s;' % x
+        else:
+            return u'&%s;' % x
+
+    def __init__(self, parser, name, attrs=None, parent=None,
+                 previous=None):
+        "Basic constructor."
+
+        # We don't actually store the parser object: that lets extracted
+        # chunks be garbage-collected
+        self.parserClass = parser.__class__
+        self.isSelfClosing = parser.isSelfClosingTag(name)
+        self.name = name
+        if attrs is None:
+            attrs = []
+        elif isinstance(attrs, dict):
+            attrs = attrs.items()
+        self.attrs = attrs
+        self.contents = []
+        self.setup(parent, previous)
+        self.hidden = False
+        self.containsSubstitutions = False
+        self.convertHTMLEntities = parser.convertHTMLEntities
+        self.convertXMLEntities = parser.convertXMLEntities
+        self.escapeUnrecognizedEntities = parser.escapeUnrecognizedEntities
+
+        # Convert any HTML, XML, or numeric entities in the attribute values.
+        convert = lambda(k, val): (k,
+                                   re.sub("&(#\d+|#x[0-9a-fA-F]+|\w+);",
+                                          self._convertEntities,
+                                          val))
+        self.attrs = map(convert, self.attrs)
+
+    def getString(self):
+        if (len(self.contents) == 1
+            and isinstance(self.contents[0], NavigableString)):
+            return self.contents[0]
+
+    def setString(self, string):
+        """Replace the contents of the tag with a string"""
+        self.clear()
+        self.append(string)
+
+    string = property(getString, setString)
+
+    def getText(self, separator=u""):
+        if not len(self.contents):
+            return u""
+        stopNode = self._lastRecursiveChild().next
+        strings = []
+        current = self.contents[0]
+        while current is not stopNode:
+            if isinstance(current, NavigableString):
+                strings.append(current.strip())
+            current = current.next
+        return separator.join(strings)
+
+    text = property(getText)
+
+    def get(self, key, default=None):
+        """Returns the value of the 'key' attribute for the tag, or
+        the value given for 'default' if it doesn't have that
+        attribute."""
+        return self._getAttrMap().get(key, default)
+
+    def clear(self):
+        """Extract all children."""
+        for child in self.contents[:]:
+            child.extract()
+
+    def index(self, element):
+        for i, child in enumerate(self.contents):
+            if child is element:
+                return i
+        raise ValueError("Tag.index: element not in tag")
+
+    def has_key(self, key):
+        return self._getAttrMap().has_key(key)
+
+    def __getitem__(self, key):
+        """tag[key] returns the value of the 'key' attribute for the tag,
+        and throws an exception if it's not there."""
+        return self._getAttrMap()[key]
+
+    def __iter__(self):
+        "Iterating over a tag iterates over its contents."
+        return iter(self.contents)
+
+    def __len__(self):
+        "The length of a tag is the length of its list of contents."
+        return len(self.contents)
+
+    def __contains__(self, x):
+        return x in self.contents
+
+    def __nonzero__(self):
+        "A tag is non-None even if it has no contents."
+        return True
+
+    def __setitem__(self, key, value):
+        """Setting tag[key] sets the value of the 'key' attribute for the
+        tag."""
+        self._getAttrMap()
+        self.attrMap[key] = value
+        found = False
+        for i in range(0, len(self.attrs)):
+            if self.attrs[i][0] == key:
+                self.attrs[i] = (key, value)
+                found = True
+        if not found:
+            self.attrs.append((key, value))
+        self._getAttrMap()[key] = value
+
+    def __delitem__(self, key):
+        "Deleting tag[key] deletes all 'key' attributes for the tag."
+        for item in self.attrs:
+            if item[0] == key:
+                self.attrs.remove(item)
+                #We don't break because bad HTML can define the same
+                #attribute multiple times.
+            self._getAttrMap()
+            if self.attrMap.has_key(key):
+                del self.attrMap[key]
+
+    def __call__(self, *args, **kwargs):
+        """Calling a tag like a function is the same as calling its
+        findAll() method. Eg. tag('a') returns a list of all the A tags
+        found within this tag."""
+        return apply(self.findAll, args, kwargs)
+
+    def __getattr__(self, tag):
+        #print "Getattr %s.%s" % (self.__class__, tag)
+        if len(tag) > 3 and tag.rfind('Tag') == len(tag)-3:
+            return self.find(tag[:-3])
+        elif tag.find('__') != 0:
+            return self.find(tag)
+        raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__, tag)
+
+    def __eq__(self, other):
+        """Returns true iff this tag has the same name, the same attributes,
+        and the same contents (recursively) as the given tag.
+
+        NOTE: right now this will return false if two tags have the
+        same attributes in a different order. Should this be fixed?"""
+        if other is self:
+            return True
+        if not hasattr(other, 'name') or not hasattr(other, 'attrs') or not hasattr(other, 'contents') or self.name != other.name or self.attrs != other.attrs or len(self) != len(other):
+            return False
+        for i in range(0, len(self.contents)):
+            if self.contents[i] != other.contents[i]:
+                return False
+        return True
+
+    def __ne__(self, other):
+        """Returns true iff this tag is not identical to the other tag,
+        as defined in __eq__."""
+        return not self == other
+
+    def __repr__(self, encoding=DEFAULT_OUTPUT_ENCODING):
+        """Renders this tag as a string."""
+        return self.__str__(encoding)
+
+    def __unicode__(self):
+        return self.__str__(None)
+
+    BARE_AMPERSAND_OR_BRACKET = re.compile("([<>]|"
+                                           + "&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)"
+                                           + ")")
+
+    def _sub_entity(self, x):
+        """Used with a regular expression to substitute the
+        appropriate XML entity for an XML special character."""
+        return "&" + self.XML_SPECIAL_CHARS_TO_ENTITIES[x.group(0)[0]] + ";"
+
+    def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING,
+                prettyPrint=False, indentLevel=0):
+        """Returns a string or Unicode representation of this tag and
+        its contents. To get Unicode, pass None for encoding.
+
+        NOTE: since Python's HTML parser consumes whitespace, this
+        method is not certain to reproduce the whitespace present in
+        the original string."""
+
+        encodedName = self.toEncoding(self.name, encoding)
+
+        attrs = []
+        if self.attrs:
+            for key, val in self.attrs:
+                fmt = '%s="%s"'
+                if isinstance(val, basestring):
+                    if self.containsSubstitutions and '%SOUP-ENCODING%' in val:
+                        val = self.substituteEncoding(val, encoding)
+
+                    # The attribute value either:
+                    #
+                    # * Contains no embedded double quotes or single quotes.
+                    #   No problem: we enclose it in double quotes.
+                    # * Contains embedded single quotes. No problem:
+                    #   double quotes work here too.
+                    # * Contains embedded double quotes. No problem:
+                    #   we enclose it in single quotes.
+                    # * Embeds both single _and_ double quotes. This
+                    #   can't happen naturally, but it can happen if
+                    #   you modify an attribute value after parsing
+                    #   the document. Now we have a bit of a
+                    #   problem. We solve it by enclosing the
+                    #   attribute in single quotes, and escaping any
+                    #   embedded single quotes to XML entities.
+                    if '"' in val:
+                        fmt = "%s='%s'"
+                        if "'" in val:
+                            # TODO: replace with apos when
+                            # appropriate.
+                            val = val.replace("'", "&squot;")
+
+                    # Now we're okay w/r/t quotes. But the attribute
+                    # value might also contain angle brackets, or
+                    # ampersands that aren't part of entities. We need
+                    # to escape those to XML entities too.
+                    val = self.BARE_AMPERSAND_OR_BRACKET.sub(self._sub_entity, val)
+
+                attrs.append(fmt % (self.toEncoding(key, encoding),
+                                    self.toEncoding(val, encoding)))
+        close = ''
+        closeTag = ''
+        if self.isSelfClosing:
+            close = ' /'
+        else:
+            closeTag = '</%s>' % encodedName
+
+        indentTag, indentContents = 0, 0
+        if prettyPrint:
+            indentTag = indentLevel
+            space = (' ' * (indentTag-1))
+            indentContents = indentTag + 1
+        contents = self.renderContents(encoding, prettyPrint, indentContents)
+        if self.hidden:
+            s = contents
+        else:
+            s = []
+            attributeString = ''
+            if attrs:
+                attributeString = ' ' + ' '.join(attrs)
+            if prettyPrint:
+                s.append(space)
+            s.append('<%s%s%s>' % (encodedName, attributeString, close))
+            if prettyPrint:
+                s.append("\n")
+            s.append(contents)
+            if prettyPrint and contents and contents[-1] != "\n":
+                s.append("\n")
+            if prettyPrint and closeTag:
+                s.append(space)
+            s.append(closeTag)
+            if prettyPrint and closeTag and self.nextSibling:
+                s.append("\n")
+            s = ''.join(s)
+        return s
+
+    def decompose(self):
+        """Recursively destroys the contents of this tree."""
+        self.extract()
+        if len(self.contents) == 0:
+            return
+        current = self.contents[0]
+        while current is not None:
+            next = current.next
+            if isinstance(current, Tag):
+                del current.contents[:]
+            current.parent = None
+            current.previous = None
+            current.previousSibling = None
+            current.next = None
+            current.nextSibling = None
+            current = next
+
+    def prettify(self, encoding=DEFAULT_OUTPUT_ENCODING):
+        return self.__str__(encoding, True)
+
+    def renderContents(self, encoding=DEFAULT_OUTPUT_ENCODING,
+                       prettyPrint=False, indentLevel=0):
+        """Renders the contents of this tag as a string in the given
+        encoding. If encoding is None, returns a Unicode string.."""
+        s=[]
+        for c in self:
+            text = None
+            if isinstance(c, NavigableString):
+                text = c.__str__(encoding)
+            elif isinstance(c, Tag):
+                s.append(c.__str__(encoding, prettyPrint, indentLevel))
+            if text and prettyPrint:
+                text = text.strip()
+            if text:
+                if prettyPrint:
+                    s.append(" " * (indentLevel-1))
+                s.append(text)
+                if prettyPrint:
+                    s.append("\n")
+        return ''.join(s)
+
+    #Soup methods
+
+    def find(self, name=None, attrs={}, recursive=True, text=None,
+             **kwargs):
+        """Return only the first child of this Tag matching the given
+        criteria."""
+        r = None
+        l = self.findAll(name, attrs, recursive, text, 1, **kwargs)
+        if l:
+            r = l[0]
+        return r
+    findChild = find
+
+    def findAll(self, name=None, attrs={}, recursive=True, text=None,
+                limit=None, **kwargs):
+        """Extracts a list of Tag objects that match the given
+        criteria.  You can specify the name of the Tag and any
+        attributes you want the Tag to have.
+
+        The value of a key-value pair in the 'attrs' map can be a
+        string, a list of strings, a regular expression object, or a
+        callable that takes a string and returns whether or not the
+        string matches for some custom definition of 'matches'. The
+        same is true of the tag name."""
+        generator = self.recursiveChildGenerator
+        if not recursive:
+            generator = self.childGenerator
+        return self._findAll(name, attrs, text, limit, generator, **kwargs)
+    findChildren = findAll
+
+    # Pre-3.x compatibility methods
+    first = find
+    fetch = findAll
+
+    def fetchText(self, text=None, recursive=True, limit=None):
+        return self.findAll(text=text, recursive=recursive, limit=limit)
+
+    def firstText(self, text=None, recursive=True):
+        return self.find(text=text, recursive=recursive)
+
+    #Private methods
+
+    def _getAttrMap(self):
+        """Initializes a map representation of this tag's attributes,
+        if not already initialized."""
+        if not getattr(self, 'attrMap'):
+            self.attrMap = {}
+            for (key, value) in self.attrs:
+                self.attrMap[key] = value
+        return self.attrMap
+
+    #Generator methods
+    def childGenerator(self):
+        # Just use the iterator from the contents
+        return iter(self.contents)
+
+    def recursiveChildGenerator(self):
+        if not len(self.contents):
+            raise StopIteration
+        stopNode = self._lastRecursiveChild().next
+        current = self.contents[0]
+        while current is not stopNode:
+            yield current
+            current = current.next
+
+
+# Next, a couple classes to represent queries and their results.
+class SoupStrainer:
+    """Encapsulates a number of ways of matching a markup element (tag or
+    text)."""
+
+    def __init__(self, name=None, attrs={}, text=None, **kwargs):
+        self.name = name
+        if isinstance(attrs, basestring):
+            kwargs['class'] = _match_css_class(attrs)
+            attrs = None
+        if kwargs:
+            if attrs:
+                attrs = attrs.copy()
+                attrs.update(kwargs)
+            else:
+                attrs = kwargs
+        self.attrs = attrs
+        self.text = text
+
+    def __str__(self):
+        if self.text:
+            return self.text
+        else:
+            return "%s|%s" % (self.name, self.attrs)
+
+    def searchTag(self, markupName=None, markupAttrs={}):
+        found = None
+        markup = None
+        if isinstance(markupName, Tag):
+            markup = markupName
+            markupAttrs = markup
+        callFunctionWithTagData = callable(self.name) \
+                                and not isinstance(markupName, Tag)
+
+        if (not self.name) \
+               or callFunctionWithTagData \
+               or (markup and self._matches(markup, self.name)) \
+               or (not markup and self._matches(markupName, self.name)):
+            if callFunctionWithTagData:
+                match = self.name(markupName, markupAttrs)
+            else:
+                match = True
+                markupAttrMap = None
+                for attr, matchAgainst in self.attrs.items():
+                    if not markupAttrMap:
+                         if hasattr(markupAttrs, 'get'):
+                            markupAttrMap = markupAttrs
+                         else:
+                            markupAttrMap = {}
+                            for k,v in markupAttrs:
+                                markupAttrMap[k] = v
+                    attrValue = markupAttrMap.get(attr)
+                    if not self._matches(attrValue, matchAgainst):
+                        match = False
+                        break
+            if match:
+                if markup:
+                    found = markup
+                else:
+                    found = markupName
+        return found
+
+    def search(self, markup):
+        #print 'looking for %s in %s' % (self, markup)
+        found = None
+        # If given a list of items, scan it for a text element that
+        # matches.
+        if hasattr(markup, "__iter__") \
+                and not isinstance(markup, Tag):
+            for element in markup:
+                if isinstance(element, NavigableString) \
+                       and self.search(element):
+                    found = element
+                    break
+        # If it's a Tag, make sure its name or attributes match.
+        # Don't bother with Tags if we're searching for text.
+        elif isinstance(markup, Tag):
+            if not self.text:
+                found = self.searchTag(markup)
+        # If it's text, make sure the text matches.
+        elif isinstance(markup, NavigableString) or \
+                 isinstance(markup, basestring):
+            if self._matches(markup, self.text):
+                found = markup
+        else:
+            raise Exception, "I don't know how to match against a %s" \
+                  % markup.__class__
+        return found
+
+    def _matches(self, markup, matchAgainst):
+        #print "Matching %s against %s" % (markup, matchAgainst)
+        result = False
+        if matchAgainst is True:
+            result = markup is not None
+        elif callable(matchAgainst):
+            result = matchAgainst(markup)
+        else:
+            #Custom match methods take the tag as an argument, but all
+            #other ways of matching match the tag name as a string.
+            if isinstance(markup, Tag):
+                markup = markup.name
+            if markup and not isinstance(markup, basestring):
+                markup = unicode(markup)
+            #Now we know that chunk is either a string, or None.
+            if hasattr(matchAgainst, 'match'):
+                # It's a regexp object.
+                result = markup and matchAgainst.search(markup)
+            elif hasattr(matchAgainst, '__iter__'): # list-like
+                result = markup in matchAgainst
+            elif hasattr(matchAgainst, 'items'):
+                result = markup.has_key(matchAgainst)
+            elif matchAgainst and isinstance(markup, basestring):
+                if isinstance(markup, unicode):
+                    matchAgainst = unicode(matchAgainst)
+                else:
+                    matchAgainst = str(matchAgainst)
+
+            if not result:
+                result = matchAgainst == markup
+        return result
+
+class ResultSet(list):
+    """A ResultSet is just a list that keeps track of the SoupStrainer
+    that created it."""
+    def __init__(self, source):
+        list.__init__([])
+        self.source = source
+
+# Now, some helper functions.
+
+def buildTagMap(default, *args):
+    """Turns a list of maps, lists, or scalars into a single map.
+    Used to build the SELF_CLOSING_TAGS, NESTABLE_TAGS, and
+    NESTING_RESET_TAGS maps out of lists and partial maps."""
+    built = {}
+    for portion in args:
+        if hasattr(portion, 'items'):
+            #It's a map. Merge it.
+            for k,v in portion.items():
+                built[k] = v
+        elif hasattr(portion, '__iter__'): # is a list
+            #It's a list. Map each item to the default.
+            for k in portion:
+                built[k] = default
+        else:
+            #It's a scalar. Map it to the default.
+            built[portion] = default
+    return built
+
+# Now, the parser classes.
+
+class BeautifulStoneSoup(Tag, SGMLParser):
+
+    """This class contains the basic parser and search code. It defines
+    a parser that knows nothing about tag behavior except for the
+    following:
+
+      You can't close a tag without closing all the tags it encloses.
+      That is, "<foo><bar></foo>" actually means
+      "<foo><bar></bar></foo>".
+
+    [Another possible explanation is "<foo><bar /></foo>", but since
+    this class defines no SELF_CLOSING_TAGS, it will never use that
+    explanation.]
+
+    This class is useful for parsing XML or made-up markup languages,
+    or when BeautifulSoup makes an assumption counter to what you were
+    expecting."""
+
+    SELF_CLOSING_TAGS = {}
+    NESTABLE_TAGS = {}
+    RESET_NESTING_TAGS = {}
+    QUOTE_TAGS = {}
+    PRESERVE_WHITESPACE_TAGS = []
+
+    MARKUP_MASSAGE = [(re.compile('(<[^<>]*)/>'),
+                       lambda x: x.group(1) + ' />'),
+                      (re.compile('<!\s+([^<>]*)>'),
+                       lambda x: '<!' + x.group(1) + '>')
+                      ]
+
+    ROOT_TAG_NAME = u'[document]'
+
+    HTML_ENTITIES = "html"
+    XML_ENTITIES = "xml"
+    XHTML_ENTITIES = "xhtml"
+    # TODO: This only exists for backwards-compatibility
+    ALL_ENTITIES = XHTML_ENTITIES
+
+    # Used when determining whether a text node is all whitespace and
+    # can be replaced with a single space. A text node that contains
+    # fancy Unicode spaces (usually non-breaking) should be left
+    # alone.
+    STRIP_ASCII_SPACES = { 9: None, 10: None, 12: None, 13: None, 32: None, }
+
+    def __init__(self, markup="", parseOnlyThese=None, fromEncoding=None,
+                 markupMassage=True, smartQuotesTo=XML_ENTITIES,
+                 convertEntities=None, selfClosingTags=None, isHTML=False):
+        """The Soup object is initialized as the 'root tag', and the
+        provided markup (which can be a string or a file-like object)
+        is fed into the underlying parser.
+
+        sgmllib will process most bad HTML, and the BeautifulSoup
+        class has some tricks for dealing with some HTML that kills
+        sgmllib, but Beautiful Soup can nonetheless choke or lose data
+        if your data uses self-closing tags or declarations
+        incorrectly.
+
+        By default, Beautiful Soup uses regexes to sanitize input,
+        avoiding the vast majority of these problems. If the problems
+        don't apply to you, pass in False for markupMassage, and
+        you'll get better performance.
+
+        The default parser massage techniques fix the two most common
+        instances of invalid HTML that choke sgmllib:
+
+         <br/> (No space between name of closing tag and tag close)
+         <! --Comment--> (Extraneous whitespace in declaration)
+
+        You can pass in a custom list of (RE object, replace method)
+        tuples to get Beautiful Soup to scrub your input the way you
+        want."""
+
+        self.parseOnlyThese = parseOnlyThese
+        self.fromEncoding = fromEncoding
+        self.smartQuotesTo = smartQuotesTo
+        self.convertEntities = convertEntities
+        # Set the rules for how we'll deal with the entities we
+        # encounter
+        if self.convertEntities:
+            # It doesn't make sense to convert encoded characters to
+            # entities even while you're converting entities to Unicode.
+            # Just convert it all to Unicode.
+            self.smartQuotesTo = None
+            if convertEntities == self.HTML_ENTITIES:
+                self.convertXMLEntities = False
+                self.convertHTMLEntities = True
+                self.escapeUnrecognizedEntities = True
+            elif convertEntities == self.XHTML_ENTITIES:
+                self.convertXMLEntities = True
+                self.convertHTMLEntities = True
+                self.escapeUnrecognizedEntities = False
+            elif convertEntities == self.XML_ENTITIES:
+                self.convertXMLEntities = True
+                self.convertHTMLEntities = False
+                self.escapeUnrecognizedEntities = False
+        else:
+            self.convertXMLEntities = False
+            self.convertHTMLEntities = False
+            self.escapeUnrecognizedEntities = False
+
+        self.instanceSelfClosingTags = buildTagMap(None, selfClosingTags)
+        SGMLParser.__init__(self)
+
+        if hasattr(markup, 'read'):        # It's a file-type object.
+            markup = markup.read()
+        self.markup = markup
+        self.markupMassage = markupMassage
+        try:
+            self._feed(isHTML=isHTML)
+        except StopParsing:
+            pass
+        self.markup = None                 # The markup can now be GCed
+
+    def convert_charref(self, name):
+        """This method fixes a bug in Python's SGMLParser."""
+        try:
+            n = int(name)
+        except ValueError:
+            return
+        if not 0 <= n <= 127 : # ASCII ends at 127, not 255
+            return
+        return self.convert_codepoint(n)
+
+    def _feed(self, inDocumentEncoding=None, isHTML=False):
+        # Convert the document to Unicode.
+        markup = self.markup
+        if isinstance(markup, unicode):
+            if not hasattr(self, 'originalEncoding'):
+                self.originalEncoding = None
+        else:
+            dammit = UnicodeDammit\
+                     (markup, [self.fromEncoding, inDocumentEncoding],
+                      smartQuotesTo=self.smartQuotesTo, isHTML=isHTML)
+            markup = dammit.unicode
+            self.originalEncoding = dammit.originalEncoding
+            self.declaredHTMLEncoding = dammit.declaredHTMLEncoding
+        if markup:
+            if self.markupMassage:
+                if not hasattr(self.markupMassage, "__iter__"):
+                    self.markupMassage = self.MARKUP_MASSAGE
+                for fix, m in self.markupMassage:
+                    markup = fix.sub(m, markup)
+                # TODO: We get rid of markupMassage so that the
+                # soup object can be deepcopied later on. Some
+                # Python installations can't copy regexes. If anyone
+                # was relying on the existence of markupMassage, this
+                # might cause problems.
+                del(self.markupMassage)
+        self.reset()
+
+        SGMLParser.feed(self, markup)
+        # Close out any unfinished strings and close all the open tags.
+        self.endData()
+        while self.currentTag.name != self.ROOT_TAG_NAME:
+            self.popTag()
+
+    def __getattr__(self, methodName):
+        """This method routes method call requests to either the SGMLParser
+        superclass or the Tag superclass, depending on the method name."""
+        #print "__getattr__ called on %s.%s" % (self.__class__, methodName)
+
+        if methodName.startswith('start_') or methodName.startswith('end_') \
+               or methodName.startswith('do_'):
+            return SGMLParser.__getattr__(self, methodName)
+        elif not methodName.startswith('__'):
+            return Tag.__getattr__(self, methodName)
+        else:
+            raise AttributeError
+
+    def isSelfClosingTag(self, name):
+        """Returns true iff the given string is the name of a
+        self-closing tag according to this parser."""
+        return self.SELF_CLOSING_TAGS.has_key(name) \
+               or self.instanceSelfClosingTags.has_key(name)
+
+    def reset(self):
+        Tag.__init__(self, self, self.ROOT_TAG_NAME)
+        self.hidden = 1
+        SGMLParser.reset(self)
+        self.currentData = []
+        self.currentTag = None
+        self.tagStack = []
+        self.quoteStack = []
+        self.pushTag(self)
+
+    def popTag(self):
+        tag = self.tagStack.pop()
+
+        #print "Pop", tag.name
+        if self.tagStack:
+            self.currentTag = self.tagStack[-1]
+        return self.currentTag
+
+    def pushTag(self, tag):
+        #print "Push", tag.name
+        if self.currentTag:
+            self.currentTag.contents.append(tag)
+        self.tagStack.append(tag)
+        self.currentTag = self.tagStack[-1]
+
+    def endData(self, containerClass=NavigableString):
+        if self.currentData:
+            currentData = u''.join(self.currentData)
+            if (currentData.translate(self.STRIP_ASCII_SPACES) == '' and
+                not set([tag.name for tag in self.tagStack]).intersection(
+                    self.PRESERVE_WHITESPACE_TAGS)):
+                if '\n' in currentData:
+                    currentData = '\n'
+                else:
+                    currentData = ' '
+            self.currentData = []
+            if self.parseOnlyThese and len(self.tagStack) <= 1 and \
+                   (not self.parseOnlyThese.text or \
+                    not self.parseOnlyThese.search(currentData)):
+                return
+            o = containerClass(currentData)
+            o.setup(self.currentTag, self.previous)
+            if self.previous:
+                self.previous.next = o
+            self.previous = o
+            self.currentTag.contents.append(o)
+
+
+    def _popToTag(self, name, inclusivePop=True):
+        """Pops the tag stack up to and including the most recent
+        instance of the given tag. If inclusivePop is false, pops the tag
+        stack up to but *not* including the most recent instqance of
+        the given tag."""
+        #print "Popping to %s" % name
+        if name == self.ROOT_TAG_NAME:
+            return
+
+        numPops = 0
+        mostRecentTag = None
+        for i in range(len(self.tagStack)-1, 0, -1):
+            if name == self.tagStack[i].name:
+                numPops = len(self.tagStack)-i
+                break
+        if not inclusivePop:
+            numPops = numPops - 1
+
+        for i in range(0, numPops):
+            mostRecentTag = self.popTag()
+        return mostRecentTag
+
+    def _smartPop(self, name):
+
+        """We need to pop up to the previous tag of this type, unless
+        one of this tag's nesting reset triggers comes between this
+        tag and the previous tag of this type, OR unless this tag is a
+        generic nesting trigger and another generic nesting trigger
+        comes between this tag and the previous tag of this type.
+
+        Examples:
+         <p>Foo<b>Bar *<p>* should pop to 'p', not 'b'.
+         <p>Foo<table>Bar *<p>* should pop to 'table', not 'p'.
+         <p>Foo<table><tr>Bar *<p>* should pop to 'tr', not 'p'.
+
+         <li><ul><li> *<li>* should pop to 'ul', not the first 'li'.
+         <tr><table><tr> *<tr>* should pop to 'table', not the first 'tr'
+         <td><tr><td> *<td>* should pop to 'tr', not the first 'td'
+        """
+
+        nestingResetTriggers = self.NESTABLE_TAGS.get(name)
+        isNestable = nestingResetTriggers != None
+        isResetNesting = self.RESET_NESTING_TAGS.has_key(name)
+        popTo = None
+        inclusive = True
+        for i in range(len(self.tagStack)-1, 0, -1):
+            p = self.tagStack[i]
+            if (not p or p.name == name) and not isNestable:
+                #Non-nestable tags get popped to the top or to their
+                #last occurance.
+                popTo = name
+                break
+            if (nestingResetTriggers is not None
+                and p.name in nestingResetTriggers) \
+                or (nestingResetTriggers is None and isResetNesting
+                    and self.RESET_NESTING_TAGS.has_key(p.name)):
+
+                #If we encounter one of the nesting reset triggers
+                #peculiar to this tag, or we encounter another tag
+                #that causes nesting to reset, pop up to but not
+                #including that tag.
+                popTo = p.name
+                inclusive = False
+                break
+            p = p.parent
+        if popTo:
+            self._popToTag(popTo, inclusive)
+
+    def unknown_starttag(self, name, attrs, selfClosing=0):
+        #print "Start tag %s: %s" % (name, attrs)
+        if self.quoteStack:
+            #This is not a real tag.
+            #print "<%s> is not real!" % name
+            attrs = ''.join([' %s="%s"' % (x, y) for x, y in attrs])
+            self.handle_data('<%s%s>' % (name, attrs))
+            return
+        self.endData()
+
+        if not self.isSelfClosingTag(name) and not selfClosing:
+            self._smartPop(name)
+
+        if self.parseOnlyThese and len(self.tagStack) <= 1 \
+               and (self.parseOnlyThese.text or not self.parseOnlyThese.searchTag(name, attrs)):
+            return
+
+        tag = Tag(self, name, attrs, self.currentTag, self.previous)
+        if self.previous:
+            self.previous.next = tag
+        self.previous = tag
+        self.pushTag(tag)
+        if selfClosing or self.isSelfClosingTag(name):
+            self.popTag()
+        if name in self.QUOTE_TAGS:
+            #print "Beginning quote (%s)" % name
+            self.quoteStack.append(name)
+            self.literal = 1
+        return tag
+
+    def unknown_endtag(self, name):
+        #print "End tag %s" % name
+        if self.quoteStack and self.quoteStack[-1] != name:
+            #This is not a real end tag.
+            #print "</%s> is not real!" % name
+            self.handle_data('</%s>' % name)
+            return
+        self.endData()
+        self._popToTag(name)
+        if self.quoteStack and self.quoteStack[-1] == name:
+            self.quoteStack.pop()
+            self.literal = (len(self.quoteStack) > 0)
+
+    def handle_data(self, data):
+        self.currentData.append(data)
+
+    def _toStringSubclass(self, text, subclass):
+        """Adds a certain piece of text to the tree as a NavigableString
+        subclass."""
+        self.endData()
+        self.handle_data(text)
+        self.endData(subclass)
+
+    def handle_pi(self, text):
+        """Handle a processing instruction as a ProcessingInstruction
+        object, possibly one with a %SOUP-ENCODING% slot into which an
+        encoding will be plugged later."""
+        if text[:3] == "xml":
+            text = u"xml version='1.0' encoding='%SOUP-ENCODING%'"
+        self._toStringSubclass(text, ProcessingInstruction)
+
+    def handle_comment(self, text):
+        "Handle comments as Comment objects."
+        self._toStringSubclass(text, Comment)
+
+    def handle_charref(self, ref):
+        "Handle character references as data."
+        if self.convertEntities:
+            data = unichr(int(ref))
+        else:
+            data = '&#%s;' % ref
+        self.handle_data(data)
+
+    def handle_entityref(self, ref):
+        """Handle entity references as data, possibly converting known
+        HTML and/or XML entity references to the corresponding Unicode
+        characters."""
+        data = None
+        if self.convertHTMLEntities:
+            try:
+                data = unichr(name2codepoint[ref])
+            except KeyError:
+                pass
+
+        if not data and self.convertXMLEntities:
+                data = self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref)
+
+        if not data and self.convertHTMLEntities and \
+            not self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref):
+                # TODO: We've got a problem here. We're told this is
+                # an entity reference, but it's not an XML entity
+                # reference or an HTML entity reference. Nonetheless,
+                # the logical thing to do is to pass it through as an
+                # unrecognized entity reference.
+                #
+                # Except: when the input is "&carol;" this function
+                # will be called with input "carol". When the input is
+                # "AT&T", this function will be called with input
+                # "T". We have no way of knowing whether a semicolon
+                # was present originally, so we don't know whether
+                # this is an unknown entity or just a misplaced
+                # ampersand.
+                #
+                # The more common case is a misplaced ampersand, so I
+                # escape the ampersand and omit the trailing semicolon.
+                data = "&amp;%s" % ref
+        if not data:
+            # This case is different from the one above, because we
+            # haven't already gone through a supposedly comprehensive
+            # mapping of entities to Unicode characters. We might not
+            # have gone through any mapping at all. So the chances are
+            # very high that this is a real entity, and not a
+            # misplaced ampersand.
+            data = "&%s;" % ref
+        self.handle_data(data)
+
+    def handle_decl(self, data):
+        "Handle DOCTYPEs and the like as Declaration objects."
+        self._toStringSubclass(data, Declaration)
+
+    def parse_declaration(self, i):
+        """Treat a bogus SGML declaration as raw data. Treat a CDATA
+        declaration as a CData object."""
+        j = None
+        if self.rawdata[i:i+9] == '<![CDATA[':
+             k = self.rawdata.find(']]>', i)
+             if k == -1:
+                 k = len(self.rawdata)
+             data = self.rawdata[i+9:k]
+             j = k+3
+             self._toStringSubclass(data, CData)
+        else:
+            try:
+                j = SGMLParser.parse_declaration(self, i)
+            except SGMLParseError:
+                toHandle = self.rawdata[i:]
+                self.handle_data(toHandle)
+                j = i + len(toHandle)
+        return j
+
+class BeautifulSoup(BeautifulStoneSoup):
+
+    """This parser knows the following facts about HTML:
+
+    * Some tags have no closing tag and should be interpreted as being
+      closed as soon as they are encountered.
+
+    * The text inside some tags (ie. 'script') may contain tags which
+      are not really part of the document and which should be parsed
+      as text, not tags. If you want to parse the text as tags, you can
+      always fetch it and parse it explicitly.
+
+    * Tag nesting rules:
+
+      Most tags can't be nested at all. For instance, the occurance of
+      a <p> tag should implicitly close the previous <p> tag.
+
+       <p>Para1<p>Para2
+        should be transformed into:
+       <p>Para1</p><p>Para2
+
+      Some tags can be nested arbitrarily. For instance, the occurance
+      of a <blockquote> tag should _not_ implicitly close the previous
+      <blockquote> tag.
+
+       Alice said: <blockquote>Bob said: <blockquote>Blah
+        should NOT be transformed into:
+       Alice said: <blockquote>Bob said: </blockquote><blockquote>Blah
+
+      Some tags can be nested, but the nesting is reset by the
+      interposition of other tags. For instance, a <tr> tag should
+      implicitly close the previous <tr> tag within the same <table>,
+      but not close a <tr> tag in another table.
+
+       <table><tr>Blah<tr>Blah
+        should be transformed into:
+       <table><tr>Blah</tr><tr>Blah
+        but,
+       <tr>Blah<table><tr>Blah
+        should NOT be transformed into
+       <tr>Blah<table></tr><tr>Blah
+
+    Differing assumptions about tag nesting rules are a major source
+    of problems with the BeautifulSoup class. If BeautifulSoup is not
+    treating as nestable a tag your page author treats as nestable,
+    try ICantBelieveItsBeautifulSoup, MinimalSoup, or
+    BeautifulStoneSoup before writing your own subclass."""
+
+    def __init__(self, *args, **kwargs):
+        if not kwargs.has_key('smartQuotesTo'):
+            kwargs['smartQuotesTo'] = self.HTML_ENTITIES
+        kwargs['isHTML'] = True
+        BeautifulStoneSoup.__init__(self, *args, **kwargs)
+
+    SELF_CLOSING_TAGS = buildTagMap(None,
+                                    ('br' , 'hr', 'input', 'img', 'meta',
+                                    'spacer', 'link', 'frame', 'base', 'col'))
+
+    PRESERVE_WHITESPACE_TAGS = set(['pre', 'textarea'])
+
+    QUOTE_TAGS = {'script' : None, 'textarea' : None}
+
+    #According to the HTML standard, each of these inline tags can
+    #contain another tag of the same type. Furthermore, it's common
+    #to actually use these tags this way.
+    NESTABLE_INLINE_TAGS = ('span', 'font', 'q', 'object', 'bdo', 'sub', 'sup',
+                            'center')
+
+    #According to the HTML standard, these block tags can contain
+    #another tag of the same type. Furthermore, it's common
+    #to actually use these tags this way.
+    NESTABLE_BLOCK_TAGS = ('blockquote', 'div', 'fieldset', 'ins', 'del')
+
+    #Lists can contain other lists, but there are restrictions.
+    NESTABLE_LIST_TAGS = { 'ol' : [],
+                           'ul' : [],
+                           'li' : ['ul', 'ol'],
+                           'dl' : [],
+                           'dd' : ['dl'],
+                           'dt' : ['dl'] }
+
+    #Tables can contain other tables, but there are restrictions.
+    NESTABLE_TABLE_TAGS = {'table' : [],
+                           'tr' : ['table', 'tbody', 'tfoot', 'thead'],
+                           'td' : ['tr'],
+                           'th' : ['tr'],
+                           'thead' : ['table'],
+                           'tbody' : ['table'],
+                           'tfoot' : ['table'],
+                           }
+
+    NON_NESTABLE_BLOCK_TAGS = ('address', 'form', 'p', 'pre')
+
+    #If one of these tags is encountered, all tags up to the next tag of
+    #this type are popped.
+    RESET_NESTING_TAGS = buildTagMap(None, NESTABLE_BLOCK_TAGS, 'noscript',
+                                     NON_NESTABLE_BLOCK_TAGS,
+                                     NESTABLE_LIST_TAGS,
+                                     NESTABLE_TABLE_TAGS)
+
+    NESTABLE_TAGS = buildTagMap([], NESTABLE_INLINE_TAGS, NESTABLE_BLOCK_TAGS,
+                                NESTABLE_LIST_TAGS, NESTABLE_TABLE_TAGS)
+
+    # Used to detect the charset in a META tag; see start_meta
+    CHARSET_RE = re.compile("((^|;)\s*charset=)([^;]*)", re.M)
+
+    def start_meta(self, attrs):
+        """Beautiful Soup can detect a charset included in a META tag,
+        try to convert the document to that charset, and re-parse the
+        document from the beginning."""
+        httpEquiv = None
+        contentType = None
+        contentTypeIndex = None
+        tagNeedsEncodingSubstitution = False
+
+        for i in range(0, len(attrs)):
+            key, value = attrs[i]
+            key = key.lower()
+            if key == 'http-equiv':
+                httpEquiv = value
+            elif key == 'content':
+                contentType = value
+                contentTypeIndex = i
+
+        if httpEquiv and contentType: # It's an interesting meta tag.
+            match = self.CHARSET_RE.search(contentType)
+            if match:
+                if (self.declaredHTMLEncoding is not None or
+                    self.originalEncoding == self.fromEncoding):
+                    # An HTML encoding was sniffed while converting
+                    # the document to Unicode, or an HTML encoding was
+                    # sniffed during a previous pass through the
+                    # document, or an encoding was specified
+                    # explicitly and it worked. Rewrite the meta tag.
+                    def rewrite(match):
+                        return match.group(1) + "%SOUP-ENCODING%"
+                    newAttr = self.CHARSET_RE.sub(rewrite, contentType)
+                    attrs[contentTypeIndex] = (attrs[contentTypeIndex][0],
+                                               newAttr)
+                    tagNeedsEncodingSubstitution = True
+                else:
+                    # This is our first pass through the document.
+                    # Go through it again with the encoding information.
+                    newCharset = match.group(3)
+                    if newCharset and newCharset != self.originalEncoding:
+                        self.declaredHTMLEncoding = newCharset
+                        self._feed(self.declaredHTMLEncoding)
+                        raise StopParsing
+                    pass
+        tag = self.unknown_starttag("meta", attrs)
+        if tag and tagNeedsEncodingSubstitution:
+            tag.containsSubstitutions = True
+
+class StopParsing(Exception):
+    pass
+
+class ICantBelieveItsBeautifulSoup(BeautifulSoup):
+
+    """The BeautifulSoup class is oriented towards skipping over
+    common HTML errors like unclosed tags. However, sometimes it makes
+    errors of its own. For instance, consider this fragment:
+
+     <b>Foo<b>Bar</b></b>
+
+    This is perfectly valid (if bizarre) HTML. However, the
+    BeautifulSoup class will implicitly close the first b tag when it
+    encounters the second 'b'. It will think the author wrote
+    "<b>Foo<b>Bar", and didn't close the first 'b' tag, because
+    there's no real-world reason to bold something that's already
+    bold. When it encounters '</b></b>' it will close two more 'b'
+    tags, for a grand total of three tags closed instead of two. This
+    can throw off the rest of your document structure. The same is
+    true of a number of other tags, listed below.
+
+    It's much more common for someone to forget to close a 'b' tag
+    than to actually use nested 'b' tags, and the BeautifulSoup class
+    handles the common case. This class handles the not-co-common
+    case: where you can't believe someone wrote what they did, but
+    it's valid HTML and BeautifulSoup screwed up by assuming it
+    wouldn't be."""
+
+    I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS = \
+     ('em', 'big', 'i', 'small', 'tt', 'abbr', 'acronym', 'strong',
+      'cite', 'code', 'dfn', 'kbd', 'samp', 'strong', 'var', 'b',
+      'big')
+
+    I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS = ('noscript',)
+
+    NESTABLE_TAGS = buildTagMap([], BeautifulSoup.NESTABLE_TAGS,
+                                I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS,
+                                I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS)
+
+class MinimalSoup(BeautifulSoup):
+    """The MinimalSoup class is for parsing HTML that contains
+    pathologically bad markup. It makes no assumptions about tag
+    nesting, but it does know which tags are self-closing, that
+    <script> tags contain Javascript and should not be parsed, that
+    META tags may contain encoding information, and so on.
+
+    This also makes it better for subclassing than BeautifulStoneSoup
+    or BeautifulSoup."""
+
+    RESET_NESTING_TAGS = buildTagMap('noscript')
+    NESTABLE_TAGS = {}
+
+class BeautifulSOAP(BeautifulStoneSoup):
+    """This class will push a tag with only a single string child into
+    the tag's parent as an attribute. The attribute's name is the tag
+    name, and the value is the string child. An example should give
+    the flavor of the change:
+
+    <foo><bar>baz</bar></foo>
+     =>
+    <foo bar="baz"><bar>baz</bar></foo>
+
+    You can then access fooTag['bar'] instead of fooTag.barTag.string.
+
+    This is, of course, useful for scraping structures that tend to
+    use subelements instead of attributes, such as SOAP messages. Note
+    that it modifies its input, so don't print the modified version
+    out.
+
+    I'm not sure how many people really want to use this class; let me
+    know if you do. Mainly I like the name."""
+
+    def popTag(self):
+        if len(self.tagStack) > 1:
+            tag = self.tagStack[-1]
+            parent = self.tagStack[-2]
+            parent._getAttrMap()
+            if (isinstance(tag, Tag) and len(tag.contents) == 1 and
+                isinstance(tag.contents[0], NavigableString) and
+                not parent.attrMap.has_key(tag.name)):
+                parent[tag.name] = tag.contents[0]
+        BeautifulStoneSoup.popTag(self)
+
+#Enterprise class names! It has come to our attention that some people
+#think the names of the Beautiful Soup parser classes are too silly
+#and "unprofessional" for use in enterprise screen-scraping. We feel
+#your pain! For such-minded folk, the Beautiful Soup Consortium And
+#All-Night Kosher Bakery recommends renaming this file to
+#"RobustParser.py" (or, in cases of extreme enterprisiness,
+#"RobustParserBeanInterface.class") and using the following
+#enterprise-friendly class aliases:
+class RobustXMLParser(BeautifulStoneSoup):
+    pass
+class RobustHTMLParser(BeautifulSoup):
+    pass
+class RobustWackAssHTMLParser(ICantBelieveItsBeautifulSoup):
+    pass
+class RobustInsanelyWackAssHTMLParser(MinimalSoup):
+    pass
+class SimplifyingSOAPParser(BeautifulSOAP):
+    pass
+
+######################################################
+#
+# Bonus library: Unicode, Dammit
+#
+# This class forces XML data into a standard format (usually to UTF-8
+# or Unicode).  It is heavily based on code from Mark Pilgrim's
+# Universal Feed Parser. It does not rewrite the XML or HTML to
+# reflect a new encoding: that happens in BeautifulStoneSoup.handle_pi
+# (XML) and BeautifulSoup.start_meta (HTML).
+
+# Autodetects character encodings.
+# Download from http://chardet.feedparser.org/
+try:
+    import chardet
+#    import chardet.constants
+#    chardet.constants._debug = 1
+except ImportError:
+    chardet = None
+
+# cjkcodecs and iconv_codec make Python know about more character encodings.
+# Both are available from http://cjkpython.i18n.org/
+# They're built in if you use Python 2.4.
+try:
+    import cjkcodecs.aliases
+except ImportError:
+    pass
+try:
+    import iconv_codec
+except ImportError:
+    pass
+
+class UnicodeDammit:
+    """A class for detecting the encoding of a *ML document and
+    converting it to a Unicode string. If the source encoding is
+    windows-1252, can replace MS smart quotes with their HTML or XML
+    equivalents."""
+
+    # This dictionary maps commonly seen values for "charset" in HTML
+    # meta tags to the corresponding Python codec names. It only covers
+    # values that aren't in Python's aliases and can't be determined
+    # by the heuristics in find_codec.
+    CHARSET_ALIASES = { "macintosh" : "mac-roman",
+                        "x-sjis" : "shift-jis" }
+
+    def __init__(self, markup, overrideEncodings=[],
+                 smartQuotesTo='xml', isHTML=False):
+        self.declaredHTMLEncoding = None
+        self.markup, documentEncoding, sniffedEncoding = \
+                     self._detectEncoding(markup, isHTML)
+        self.smartQuotesTo = smartQuotesTo
+        self.triedEncodings = []
+        if markup == '' or isinstance(markup, unicode):
+            self.originalEncoding = None
+            self.unicode = unicode(markup)
+            return
+
+        u = None
+        for proposedEncoding in overrideEncodings:
+            u = self._convertFrom(proposedEncoding)
+            if u: break
+        if not u:
+            for proposedEncoding in (documentEncoding, sniffedEncoding):
+                u = self._convertFrom(proposedEncoding)
+                if u: break
+
+        # If no luck and we have auto-detection library, try that:
+        if not u and chardet and not isinstance(self.markup, unicode):
+            u = self._convertFrom(chardet.detect(self.markup)['encoding'])
+
+        # As a last resort, try utf-8 and windows-1252:
+        if not u:
+            for proposed_encoding in ("utf-8", "windows-1252"):
+                u = self._convertFrom(proposed_encoding)
+                if u: break
+
+        self.unicode = u
+        if not u: self.originalEncoding = None
+
+    def _subMSChar(self, orig):
+        """Changes a MS smart quote character to an XML or HTML
+        entity."""
+        sub = self.MS_CHARS.get(orig)
+        if isinstance(sub, tuple):
+            if self.smartQuotesTo == 'xml':
+                sub = '&#x%s;' % sub[1]
+            else:
+                sub = '&%s;' % sub[0]
+        return sub
+
+    def _convertFrom(self, proposed):
+        proposed = self.find_codec(proposed)
+        if not proposed or proposed in self.triedEncodings:
+            return None
+        self.triedEncodings.append(proposed)
+        markup = self.markup
+
+        # Convert smart quotes to HTML if coming from an encoding
+        # that might have them.
+        if self.smartQuotesTo and proposed.lower() in("windows-1252",
+                                                      "iso-8859-1",
+                                                      "iso-8859-2"):
+            markup = re.compile("([\x80-\x9f])").sub \
+                     (lambda(x): self._subMSChar(x.group(1)),
+                      markup)
+
+        try:
+            # print "Trying to convert document to %s" % proposed
+            u = self._toUnicode(markup, proposed)
+            self.markup = u
+            self.originalEncoding = proposed
+        except Exception, e:
+            # print "That didn't work!"
+            # print e
+            return None
+        #print "Correct encoding: %s" % proposed
+        return self.markup
+
+    def _toUnicode(self, data, encoding):
+        '''Given a string and its encoding, decodes the string into Unicode.
+        %encoding is a string recognized by encodings.aliases'''
+
+        # strip Byte Order Mark (if present)
+        if (len(data) >= 4) and (data[:2] == '\xfe\xff') \
+               and (data[2:4] != '\x00\x00'):
+            encoding = 'utf-16be'
+            data = data[2:]
+        elif (len(data) >= 4) and (data[:2] == '\xff\xfe') \
+                 and (data[2:4] != '\x00\x00'):
+            encoding = 'utf-16le'
+            data = data[2:]
+        elif data[:3] == '\xef\xbb\xbf':
+            encoding = 'utf-8'
+            data = data[3:]
+        elif data[:4] == '\x00\x00\xfe\xff':
+            encoding = 'utf-32be'
+            data = data[4:]
+        elif data[:4] == '\xff\xfe\x00\x00':
+            encoding = 'utf-32le'
+            data = data[4:]
+        newdata = unicode(data, encoding)
+        return newdata
+
+    def _detectEncoding(self, xml_data, isHTML=False):
+        """Given a document, tries to detect its XML encoding."""
+        xml_encoding = sniffed_xml_encoding = None
+        try:
+            if xml_data[:4] == '\x4c\x6f\xa7\x94':
+                # EBCDIC
+                xml_data = self._ebcdic_to_ascii(xml_data)
+            elif xml_data[:4] == '\x00\x3c\x00\x3f':
+                # UTF-16BE
+                sniffed_xml_encoding = 'utf-16be'
+                xml_data = unicode(xml_data, 'utf-16be').encode('utf-8')
+            elif (len(xml_data) >= 4) and (xml_data[:2] == '\xfe\xff') \
+                     and (xml_data[2:4] != '\x00\x00'):
+                # UTF-16BE with BOM
+                sniffed_xml_encoding = 'utf-16be'
+                xml_data = unicode(xml_data[2:], 'utf-16be').encode('utf-8')
+            elif xml_data[:4] == '\x3c\x00\x3f\x00':
+                # UTF-16LE
+                sniffed_xml_encoding = 'utf-16le'
+                xml_data = unicode(xml_data, 'utf-16le').encode('utf-8')
+            elif (len(xml_data) >= 4) and (xml_data[:2] == '\xff\xfe') and \
+                     (xml_data[2:4] != '\x00\x00'):
+                # UTF-16LE with BOM
+                sniffed_xml_encoding = 'utf-16le'
+                xml_data = unicode(xml_data[2:], 'utf-16le').encode('utf-8')
+            elif xml_data[:4] == '\x00\x00\x00\x3c':
+                # UTF-32BE
+                sniffed_xml_encoding = 'utf-32be'
+                xml_data = unicode(xml_data, 'utf-32be').encode('utf-8')
+            elif xml_data[:4] == '\x3c\x00\x00\x00':
+                # UTF-32LE
+                sniffed_xml_encoding = 'utf-32le'
+                xml_data = unicode(xml_data, 'utf-32le').encode('utf-8')
+            elif xml_data[:4] == '\x00\x00\xfe\xff':
+                # UTF-32BE with BOM
+                sniffed_xml_encoding = 'utf-32be'
+                xml_data = unicode(xml_data[4:], 'utf-32be').encode('utf-8')
+            elif xml_data[:4] == '\xff\xfe\x00\x00':
+                # UTF-32LE with BOM
+                sniffed_xml_encoding = 'utf-32le'
+                xml_data = unicode(xml_data[4:], 'utf-32le').encode('utf-8')
+            elif xml_data[:3] == '\xef\xbb\xbf':
+                # UTF-8 with BOM
+                sniffed_xml_encoding = 'utf-8'
+                xml_data = unicode(xml_data[3:], 'utf-8').encode('utf-8')
+            else:
+                sniffed_xml_encoding = 'ascii'
+                pass
+        except:
+            xml_encoding_match = None
+        xml_encoding_match = re.compile(
+            '^<\?.*encoding=[\'"](.*?)[\'"].*\?>').match(xml_data)
+        if not xml_encoding_match and isHTML:
+            regexp = re.compile('<\s*meta[^>]+charset=([^>]*?)[;\'">]', re.I)
+            xml_encoding_match = regexp.search(xml_data)
+        if xml_encoding_match is not None:
+            xml_encoding = xml_encoding_match.groups()[0].lower()
+            if isHTML:
+                self.declaredHTMLEncoding = xml_encoding
+            if sniffed_xml_encoding and \
+               (xml_encoding in ('iso-10646-ucs-2', 'ucs-2', 'csunicode',
+                                 'iso-10646-ucs-4', 'ucs-4', 'csucs4',
+                                 'utf-16', 'utf-32', 'utf_16', 'utf_32',
+                                 'utf16', 'u16')):
+                xml_encoding = sniffed_xml_encoding
+        return xml_data, xml_encoding, sniffed_xml_encoding
+
+
+    def find_codec(self, charset):
+        return self._codec(self.CHARSET_ALIASES.get(charset, charset)) \
+               or (charset and self._codec(charset.replace("-", ""))) \
+               or (charset and self._codec(charset.replace("-", "_"))) \
+               or charset
+
+    def _codec(self, charset):
+        if not charset: return charset
+        codec = None
+        try:
+            codecs.lookup(charset)
+            codec = charset
+        except (LookupError, ValueError):
+            pass
+        return codec
+
+    EBCDIC_TO_ASCII_MAP = None
+    def _ebcdic_to_ascii(self, s):
+        c = self.__class__
+        if not c.EBCDIC_TO_ASCII_MAP:
+            emap = (0,1,2,3,156,9,134,127,151,141,142,11,12,13,14,15,
+                    16,17,18,19,157,133,8,135,24,25,146,143,28,29,30,31,
+                    128,129,130,131,132,10,23,27,136,137,138,139,140,5,6,7,
+                    144,145,22,147,148,149,150,4,152,153,154,155,20,21,158,26,
+                    32,160,161,162,163,164,165,166,167,168,91,46,60,40,43,33,
+                    38,169,170,171,172,173,174,175,176,177,93,36,42,41,59,94,
+                    45,47,178,179,180,181,182,183,184,185,124,44,37,95,62,63,
+                    186,187,188,189,190,191,192,193,194,96,58,35,64,39,61,34,
+                    195,97,98,99,100,101,102,103,104,105,196,197,198,199,200,
+                    201,202,106,107,108,109,110,111,112,113,114,203,204,205,
+                    206,207,208,209,126,115,116,117,118,119,120,121,122,210,
+                    211,212,213,214,215,216,217,218,219,220,221,222,223,224,
+                    225,226,227,228,229,230,231,123,65,66,67,68,69,70,71,72,
+                    73,232,233,234,235,236,237,125,74,75,76,77,78,79,80,81,
+                    82,238,239,240,241,242,243,92,159,83,84,85,86,87,88,89,
+                    90,244,245,246,247,248,249,48,49,50,51,52,53,54,55,56,57,
+                    250,251,252,253,254,255)
+            import string
+            c.EBCDIC_TO_ASCII_MAP = string.maketrans( \
+            ''.join(map(chr, range(256))), ''.join(map(chr, emap)))
+        return s.translate(c.EBCDIC_TO_ASCII_MAP)
+
+    MS_CHARS = { '\x80' : ('euro', '20AC'),
+                 '\x81' : ' ',
+                 '\x82' : ('sbquo', '201A'),
+                 '\x83' : ('fnof', '192'),
+                 '\x84' : ('bdquo', '201E'),
+                 '\x85' : ('hellip', '2026'),
+                 '\x86' : ('dagger', '2020'),
+                 '\x87' : ('Dagger', '2021'),
+                 '\x88' : ('circ', '2C6'),
+                 '\x89' : ('permil', '2030'),
+                 '\x8A' : ('Scaron', '160'),
+                 '\x8B' : ('lsaquo', '2039'),
+                 '\x8C' : ('OElig', '152'),
+                 '\x8D' : '?',
+                 '\x8E' : ('#x17D', '17D'),
+                 '\x8F' : '?',
+                 '\x90' : '?',
+                 '\x91' : ('lsquo', '2018'),
+                 '\x92' : ('rsquo', '2019'),
+                 '\x93' : ('ldquo', '201C'),
+                 '\x94' : ('rdquo', '201D'),
+                 '\x95' : ('bull', '2022'),
+                 '\x96' : ('ndash', '2013'),
+                 '\x97' : ('mdash', '2014'),
+                 '\x98' : ('tilde', '2DC'),
+                 '\x99' : ('trade', '2122'),
+                 '\x9a' : ('scaron', '161'),
+                 '\x9b' : ('rsaquo', '203A'),
+                 '\x9c' : ('oelig', '153'),
+                 '\x9d' : '?',
+                 '\x9e' : ('#x17E', '17E'),
+                 '\x9f' : ('Yuml', ''),}
+
+#######################################################################
+
+
+#By default, act as an HTML pretty-printer.
+if __name__ == '__main__':
+    import sys
+    soup = BeautifulSoup(sys.stdin)
+    print soup.prettify()
diff --git a/Tools/Scripts/webkitpy/thirdparty/__init__.py b/Tools/Scripts/webkitpy/thirdparty/__init__.py
new file mode 100644
index 0000000..74ea5f6
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/__init__.py
@@ -0,0 +1,180 @@
+# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
+#
+# 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.
+
+# This module is required for Python to treat this directory as a package.
+
+"""Autoinstalls third-party code required by WebKit."""
+
+
+import codecs
+import os
+import sys
+
+from webkitpy.common.system.autoinstall import AutoInstaller
+from webkitpy.common.system.filesystem import FileSystem
+
+_THIRDPARTY_DIR = os.path.dirname(__file__)
+_AUTOINSTALLED_DIR = os.path.join(_THIRDPARTY_DIR, "autoinstalled")
+
+# Putting the autoinstall code into webkitpy/thirdparty/__init__.py
+# ensures that no autoinstalling occurs until a caller imports from
+# webkitpy.thirdparty.  This is useful if the caller wants to configure
+# logging prior to executing autoinstall code.
+
+# FIXME: If any of these servers is offline, webkit-patch breaks (and maybe
+# other scripts do, too). See <http://webkit.org/b/42080>.
+
+# We put auto-installed third-party modules in this directory--
+#
+#     webkitpy/thirdparty/autoinstalled
+fs = FileSystem()
+fs.maybe_make_directory(_AUTOINSTALLED_DIR)
+
+init_path = fs.join(_AUTOINSTALLED_DIR, "__init__.py")
+if not fs.exists(init_path):
+    fs.write_text_file(init_path, "")
+
+readme_path = fs.join(_AUTOINSTALLED_DIR, "README")
+if not fs.exists(readme_path):
+    fs.write_text_file(readme_path,
+        "This directory is auto-generated by WebKit and is "
+        "safe to delete.\nIt contains needed third-party Python "
+        "packages automatically downloaded from the web.")
+
+
+class AutoinstallImportHook(object):
+    def __init__(self, filesystem=None):
+        self._fs = filesystem or FileSystem()
+
+    def _ensure_autoinstalled_dir_is_in_sys_path(self):
+        # Some packages require that the are being put somewhere under a directory in sys.path.
+        if not _AUTOINSTALLED_DIR in sys.path:
+            sys.path.append(_AUTOINSTALLED_DIR)
+
+    def find_module(self, fullname, _):
+        # This method will run before each import. See http://www.python.org/dev/peps/pep-0302/
+        if '.autoinstalled' not in fullname:
+            return
+
+        # Note: all of the methods must follow the "_install_XXX" convention in
+        # order for autoinstall_everything(), below, to work properly.
+        if '.mechanize' in fullname:
+            self._install_mechanize()
+        elif '.pep8' in fullname:
+            self._install_pep8()
+        elif '.pylint' in fullname:
+            self._install_pylint()
+        elif '.coverage' in fullname:
+            self._install_coverage()
+        elif '.eliza' in fullname:
+            self._install_eliza()
+        elif '.irc' in fullname:
+            self._install_irc()
+        elif '.buildbot' in fullname:
+            self._install_buildbot()
+        elif '.webpagereplay' in fullname:
+            self._install_webpagereplay()
+
+    def _install_mechanize(self):
+        return self._install("http://pypi.python.org/packages/source/m/mechanize/mechanize-0.2.5.tar.gz",
+                             "mechanize-0.2.5/mechanize")
+
+    def _install_pep8(self):
+        return self._install("http://pypi.python.org/packages/source/p/pep8/pep8-0.5.0.tar.gz#md5=512a818af9979290cd619cce8e9c2e2b",
+                             "pep8-0.5.0/pep8.py")
+
+    def _install_pylint(self):
+        self._ensure_autoinstalled_dir_is_in_sys_path()
+        did_install_something = False
+        if not self._fs.exists(self._fs.join(_AUTOINSTALLED_DIR, "pylint")):
+            installer = AutoInstaller(target_dir=_AUTOINSTALLED_DIR)
+            did_install_something = installer.install("http://pypi.python.org/packages/source/l/logilab-common/logilab-common-0.58.1.tar.gz#md5=77298ab2d8bb8b4af9219791e7cee8ce", url_subpath="logilab-common-0.58.1", target_name="logilab/common")
+            did_install_something |= installer.install("http://pypi.python.org/packages/source/l/logilab-astng/logilab-astng-0.24.1.tar.gz#md5=ddaf66e4d85714d9c47a46d4bed406de", url_subpath="logilab-astng-0.24.1", target_name="logilab/astng")
+            did_install_something |= installer.install('http://pypi.python.org/packages/source/p/pylint/pylint-0.25.1.tar.gz#md5=728bbc2b339bc3749af013709a7f87a5', url_subpath="pylint-0.25.1", target_name="pylint")
+        return did_install_something
+
+    # autoinstalled.buildbot is used by BuildSlaveSupport/build.webkit.org-config/mastercfg_unittest.py
+    # and should ideally match the version of BuildBot used at build.webkit.org.
+    def _install_buildbot(self):
+        # The buildbot package uses jinja2, for example, in buildbot/status/web/base.py.
+        # buildbot imports jinja2 directly (as though it were installed on the system),
+        # so the search path needs to include jinja2.  We put jinja2 in
+        # its own directory so that we can include it in the search path
+        # without including other modules as a side effect.
+        jinja_dir = self._fs.join(_AUTOINSTALLED_DIR, "jinja2")
+        installer = AutoInstaller(append_to_search_path=True, target_dir=jinja_dir)
+        did_install_something = installer.install(url="http://pypi.python.org/packages/source/J/Jinja2/Jinja2-2.6.tar.gz#md5=1c49a8825c993bfdcf55bb36897d28a2",
+                                                url_subpath="Jinja2-2.6/jinja2")
+
+        SQLAlchemy_dir = self._fs.join(_AUTOINSTALLED_DIR, "sqlalchemy")
+        installer = AutoInstaller(append_to_search_path=True, target_dir=SQLAlchemy_dir)
+        did_install_something |= installer.install(url="http://pypi.python.org/packages/source/S/SQLAlchemy/SQLAlchemy-0.7.7.tar.gz#md5=ddf6df7e014cea318fa981364f3f93b9",
+                                                 url_subpath="SQLAlchemy-0.7.7/lib/sqlalchemy")
+
+        did_install_something |= self._install("http://pypi.python.org/packages/source/b/buildbot/buildbot-0.8.6p1.tar.gz#md5=b6727d2810c692062c657492bcbeac6a", "buildbot-0.8.6p1/buildbot")
+        return did_install_something
+
+    def _install_coverage(self):
+        self._ensure_autoinstalled_dir_is_in_sys_path()
+        return self._install(url="http://pypi.python.org/packages/source/c/coverage/coverage-3.5.1.tar.gz#md5=410d4c8155a4dab222f2bc51212d4a24", url_subpath="coverage-3.5.1/coverage")
+
+    def _install_eliza(self):
+        return self._install(url="http://www.adambarth.com/webkit/eliza", target_name="eliza.py")
+
+    def _install_irc(self):
+        # Since irclib and ircbot are two top-level packages, we need to import
+        # them separately.  We group them into an irc package for better
+        # organization purposes.
+        irc_dir = self._fs.join(_AUTOINSTALLED_DIR, "irc")
+        installer = AutoInstaller(target_dir=irc_dir)
+        did_install_something = installer.install(url="http://downloads.sourceforge.net/project/python-irclib/python-irclib/0.4.8/python-irclib-0.4.8.zip",
+                                                url_subpath="irclib.py")
+        did_install_something |= installer.install(url="http://downloads.sourceforge.net/project/python-irclib/python-irclib/0.4.8/python-irclib-0.4.8.zip",
+                          url_subpath="ircbot.py")
+        return did_install_something
+
+    def _install_webpagereplay(self):
+        did_install_something = False
+        if not self._fs.exists(self._fs.join(_AUTOINSTALLED_DIR, "webpagereplay")):
+            did_install_something = self._install("http://web-page-replay.googlecode.com/files/webpagereplay-1.1.2.tar.gz", "webpagereplay-1.1.2")
+            self._fs.move(self._fs.join(_AUTOINSTALLED_DIR, "webpagereplay-1.1.2"), self._fs.join(_AUTOINSTALLED_DIR, "webpagereplay"))
+
+        module_init_path = self._fs.join(_AUTOINSTALLED_DIR, "webpagereplay", "__init__.py")
+        if not self._fs.exists(module_init_path):
+            self._fs.write_text_file(module_init_path, "")
+        return did_install_something
+
+    def _install(self, url, url_subpath=None, target_name=None):
+        installer = AutoInstaller(target_dir=_AUTOINSTALLED_DIR)
+        return installer.install(url=url, url_subpath=url_subpath, target_name=target_name)
+
+
+_hook = AutoinstallImportHook()
+sys.meta_path.append(_hook)
+
+
+def autoinstall_everything():
+    install_methods = [method for method in dir(_hook.__class__) if method.startswith('_install_')]
+    did_install_something = False
+    for method in install_methods:
+        did_install_something |= getattr(_hook, method)()
+    return did_install_something
diff --git a/Tools/Scripts/webkitpy/thirdparty/__init___unittest.py b/Tools/Scripts/webkitpy/thirdparty/__init___unittest.py
new file mode 100644
index 0000000..b3eb75f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/__init___unittest.py
@@ -0,0 +1,74 @@
+#!/usr/bin/python
+# Copyright (C) 2011 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 sys
+import unittest
+
+from webkitpy.thirdparty import AutoinstallImportHook
+
+
+class ThirdpartyTest(unittest.TestCase):
+    def test_import_hook(self):
+        # Add another import hook and make sure we get called.
+        class MockImportHook(AutoinstallImportHook):
+            def __init__(self):
+                AutoinstallImportHook.__init__(self)
+                self.eliza_installed = False
+
+            def _install_eliza(self):
+                self.eliza_installed = True
+
+        mock_import_hook = MockImportHook()
+        try:
+            # The actual AutoinstallImportHook should be installed before us,
+            # so these modules will get installed before MockImportHook runs.
+            sys.meta_path.append(mock_import_hook)
+            # unused-variable, import failures - pylint: disable-msg=W0612,E0611,F0401
+            from webkitpy.thirdparty.autoinstalled import eliza
+            self.assertTrue(mock_import_hook.eliza_installed)
+
+        finally:
+            sys.meta_path.remove(mock_import_hook)
+
+    def test_imports(self):
+        # This method tests that we can actually import everything.
+        # unused-variable, import failures - pylint: disable-msg=W0612,E0611,F0401
+        import webkitpy.thirdparty.autoinstalled.buildbot
+        import webkitpy.thirdparty.autoinstalled.coverage
+        import webkitpy.thirdparty.autoinstalled.eliza
+        import webkitpy.thirdparty.autoinstalled.irc.ircbot
+        import webkitpy.thirdparty.autoinstalled.irc.irclib
+        import webkitpy.thirdparty.autoinstalled.mechanize
+        import webkitpy.thirdparty.autoinstalled.pylint
+        import webkitpy.thirdparty.autoinstalled.webpagereplay
+        import webkitpy.thirdparty.autoinstalled.pep8
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/thirdparty/mock.py b/Tools/Scripts/webkitpy/thirdparty/mock.py
new file mode 100644
index 0000000..015c19e
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mock.py
@@ -0,0 +1,309 @@
+# mock.py
+# Test tools for mocking and patching.
+# Copyright (C) 2007-2009 Michael Foord
+# E-mail: fuzzyman AT voidspace DOT org DOT uk
+
+# mock 0.6.0
+# http://www.voidspace.org.uk/python/mock/
+
+# Released subject to the BSD License
+# Please see http://www.voidspace.org.uk/python/license.shtml
+
+# 2009-11-25: Licence downloaded from above URL.
+# BEGIN DOWNLOADED LICENSE
+#
+# Copyright (c) 2003-2009, Michael Foord
+# All rights reserved.
+# E-mail : fuzzyman AT voidspace DOT org DOT uk
+#
+# 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 Michael Foord nor the name of Voidspace
+#       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.
+#
+# END DOWNLOADED LICENSE
+
+# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml
+# Comments, suggestions and bug reports welcome.
+
+
+__all__ = (
+    'Mock',
+    'patch',
+    'patch_object',
+    'sentinel',
+    'DEFAULT'
+)
+
+__version__ = '0.6.0'
+
+class SentinelObject(object):
+    def __init__(self, name):
+        self.name = name
+
+    def __repr__(self):
+        return '<SentinelObject "%s">' % self.name
+
+
+class Sentinel(object):
+    def __init__(self):
+        self._sentinels = {}
+
+    def __getattr__(self, name):
+        return self._sentinels.setdefault(name, SentinelObject(name))
+
+
+sentinel = Sentinel()
+
+DEFAULT = sentinel.DEFAULT
+
+class OldStyleClass:
+    pass
+ClassType = type(OldStyleClass)
+
+def _is_magic(name):
+    return '__%s__' % name[2:-2] == name
+
+def _copy(value):
+    if type(value) in (dict, list, tuple, set):
+        return type(value)(value)
+    return value
+
+
+class Mock(object):
+
+    def __init__(self, spec=None, side_effect=None, return_value=DEFAULT,
+                 name=None, parent=None, wraps=None):
+        self._parent = parent
+        self._name = name
+        if spec is not None and not isinstance(spec, list):
+            spec = [member for member in dir(spec) if not _is_magic(member)]
+
+        self._methods = spec
+        self._children = {}
+        self._return_value = return_value
+        self.side_effect = side_effect
+        self._wraps = wraps
+
+        self.reset_mock()
+
+
+    def reset_mock(self):
+        self.called = False
+        self.call_args = None
+        self.call_count = 0
+        self.call_args_list = []
+        self.method_calls = []
+        for child in self._children.itervalues():
+            child.reset_mock()
+        if isinstance(self._return_value, Mock):
+            self._return_value.reset_mock()
+
+
+    def __get_return_value(self):
+        if self._return_value is DEFAULT:
+            self._return_value = Mock()
+        return self._return_value
+
+    def __set_return_value(self, value):
+        self._return_value = value
+
+    return_value = property(__get_return_value, __set_return_value)
+
+
+    def __call__(self, *args, **kwargs):
+        self.called = True
+        self.call_count += 1
+        self.call_args = (args, kwargs)
+        self.call_args_list.append((args, kwargs))
+
+        parent = self._parent
+        name = self._name
+        while parent is not None:
+            parent.method_calls.append((name, args, kwargs))
+            if parent._parent is None:
+                break
+            name = parent._name + '.' + name
+            parent = parent._parent
+
+        ret_val = DEFAULT
+        if self.side_effect is not None:
+            if (isinstance(self.side_effect, Exception) or
+                isinstance(self.side_effect, (type, ClassType)) and
+                issubclass(self.side_effect, Exception)):
+                raise self.side_effect
+
+            ret_val = self.side_effect(*args, **kwargs)
+            if ret_val is DEFAULT:
+                ret_val = self.return_value
+
+        if self._wraps is not None and self._return_value is DEFAULT:
+            return self._wraps(*args, **kwargs)
+        if ret_val is DEFAULT:
+            ret_val = self.return_value
+        return ret_val
+
+
+    def __getattr__(self, name):
+        if self._methods is not None:
+            if name not in self._methods:
+                raise AttributeError("Mock object has no attribute '%s'" % name)
+        elif _is_magic(name):
+            raise AttributeError(name)
+
+        if name not in self._children:
+            wraps = None
+            if self._wraps is not None:
+                wraps = getattr(self._wraps, name)
+            self._children[name] = Mock(parent=self, name=name, wraps=wraps)
+
+        return self._children[name]
+
+
+    def assert_called_with(self, *args, **kwargs):
+        assert self.call_args == (args, kwargs), 'Expected: %s\nCalled with: %s' % ((args, kwargs), self.call_args)
+
+
+def _dot_lookup(thing, comp, import_path):
+    try:
+        return getattr(thing, comp)
+    except AttributeError:
+        __import__(import_path)
+        return getattr(thing, comp)
+
+
+def _importer(target):
+    components = target.split('.')
+    import_path = components.pop(0)
+    thing = __import__(import_path)
+
+    for comp in components:
+        import_path += ".%s" % comp
+        thing = _dot_lookup(thing, comp, import_path)
+    return thing
+
+
+class _patch(object):
+    def __init__(self, target, attribute, new, spec, create):
+        self.target = target
+        self.attribute = attribute
+        self.new = new
+        self.spec = spec
+        self.create = create
+        self.has_local = False
+
+
+    def __call__(self, func):
+        if hasattr(func, 'patchings'):
+            func.patchings.append(self)
+            return func
+
+        def patched(*args, **keywargs):
+            # don't use a with here (backwards compatability with 2.5)
+            extra_args = []
+            for patching in patched.patchings:
+                arg = patching.__enter__()
+                if patching.new is DEFAULT:
+                    extra_args.append(arg)
+            args += tuple(extra_args)
+            try:
+                return func(*args, **keywargs)
+            finally:
+                for patching in getattr(patched, 'patchings', []):
+                    patching.__exit__()
+
+        patched.patchings = [self]
+        patched.__name__ = func.__name__
+        patched.compat_co_firstlineno = getattr(func, "compat_co_firstlineno",
+                                                func.func_code.co_firstlineno)
+        return patched
+
+
+    def get_original(self):
+        target = self.target
+        name = self.attribute
+        create = self.create
+
+        original = DEFAULT
+        if _has_local_attr(target, name):
+            try:
+                original = target.__dict__[name]
+            except AttributeError:
+                # for instances of classes with slots, they have no __dict__
+                original = getattr(target, name)
+        elif not create and not hasattr(target, name):
+            raise AttributeError("%s does not have the attribute %r" % (target, name))
+        return original
+
+
+    def __enter__(self):
+        new, spec, = self.new, self.spec
+        original = self.get_original()
+        if new is DEFAULT:
+            # XXXX what if original is DEFAULT - shouldn't use it as a spec
+            inherit = False
+            if spec == True:
+                # set spec to the object we are replacing
+                spec = original
+                if isinstance(spec, (type, ClassType)):
+                    inherit = True
+            new = Mock(spec=spec)
+            if inherit:
+                new.return_value = Mock(spec=spec)
+        self.temp_original = original
+        setattr(self.target, self.attribute, new)
+        return new
+
+
+    def __exit__(self, *_):
+        if self.temp_original is not DEFAULT:
+            setattr(self.target, self.attribute, self.temp_original)
+        else:
+            delattr(self.target, self.attribute)
+        del self.temp_original
+
+
+def patch_object(target, attribute, new=DEFAULT, spec=None, create=False):
+    return _patch(target, attribute, new, spec, create)
+
+
+def patch(target, new=DEFAULT, spec=None, create=False):
+    try:
+        target, attribute = target.rsplit('.', 1)
+    except (TypeError, ValueError):
+        raise TypeError("Need a valid target to patch. You supplied: %r" % (target,))
+    target = _importer(target)
+    return _patch(target, attribute, new, spec, create)
+
+
+
+def _has_local_attr(obj, name):
+    try:
+        return name in vars(obj)
+    except TypeError:
+        # objects without a __dict__
+        return hasattr(obj, name)
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/COPYING b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/COPYING
new file mode 100644
index 0000000..989d02e
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/COPYING
@@ -0,0 +1,28 @@
+Copyright 2012, 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.
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/__init__.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/__init__.py
new file mode 100644
index 0000000..454ae0c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/__init__.py
@@ -0,0 +1,197 @@
+# Copyright 2011, 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.
+
+
+"""WebSocket extension for Apache HTTP Server.
+
+mod_pywebsocket is a WebSocket extension for Apache HTTP Server
+intended for testing or experimental purposes. mod_python is required.
+
+
+Installation
+============
+
+0. Prepare an Apache HTTP Server for which mod_python is enabled.
+
+1. Specify the following Apache HTTP Server directives to suit your
+   configuration.
+
+   If mod_pywebsocket is not in the Python path, specify the following.
+   <websock_lib> is the directory where mod_pywebsocket is installed.
+
+       PythonPath "sys.path+['<websock_lib>']"
+
+   Always specify the following. <websock_handlers> is the directory where
+   user-written WebSocket handlers are placed.
+
+       PythonOption mod_pywebsocket.handler_root <websock_handlers>
+       PythonHeaderParserHandler mod_pywebsocket.headerparserhandler
+
+   To limit the search for WebSocket handlers to a directory <scan_dir>
+   under <websock_handlers>, configure as follows:
+
+       PythonOption mod_pywebsocket.handler_scan <scan_dir>
+
+   <scan_dir> is useful in saving scan time when <websock_handlers>
+   contains many non-WebSocket handler files.
+
+   If you want to allow handlers whose canonical path is not under the root
+   directory (i.e. symbolic link is in root directory but its target is not),
+   configure as follows:
+
+       PythonOption mod_pywebsocket.allow_handlers_outside_root_dir On
+
+   Example snippet of httpd.conf:
+   (mod_pywebsocket is in /websock_lib, WebSocket handlers are in
+   /websock_handlers, port is 80 for ws, 443 for wss.)
+
+       <IfModule python_module>
+         PythonPath "sys.path+['/websock_lib']"
+         PythonOption mod_pywebsocket.handler_root /websock_handlers
+         PythonHeaderParserHandler mod_pywebsocket.headerparserhandler
+       </IfModule>
+
+2. Tune Apache parameters for serving WebSocket. We'd like to note that at
+   least TimeOut directive from core features and RequestReadTimeout
+   directive from mod_reqtimeout should be modified not to kill connections
+   in only a few seconds of idle time.
+
+3. Verify installation. You can use example/console.html to poke the server.
+
+
+Writing WebSocket handlers
+==========================
+
+When a WebSocket request comes in, the resource name
+specified in the handshake is considered as if it is a file path under
+<websock_handlers> and the handler defined in
+<websock_handlers>/<resource_name>_wsh.py is invoked.
+
+For example, if the resource name is /example/chat, the handler defined in
+<websock_handlers>/example/chat_wsh.py is invoked.
+
+A WebSocket handler is composed of the following three functions:
+
+    web_socket_do_extra_handshake(request)
+    web_socket_transfer_data(request)
+    web_socket_passive_closing_handshake(request)
+
+where:
+    request: mod_python request.
+
+web_socket_do_extra_handshake is called during the handshake after the
+headers are successfully parsed and WebSocket properties (ws_location,
+ws_origin, and ws_resource) are added to request. A handler
+can reject the request by raising an exception.
+
+A request object has the following properties that you can use during the
+extra handshake (web_socket_do_extra_handshake):
+- ws_resource
+- ws_origin
+- ws_version
+- ws_location (HyBi 00 only)
+- ws_extensions (HyBi 06 and later)
+- ws_deflate (HyBi 06 and later)
+- ws_protocol
+- ws_requested_protocols (HyBi 06 and later)
+
+The last two are a bit tricky. See the next subsection.
+
+
+Subprotocol Negotiation
+-----------------------
+
+For HyBi 06 and later, ws_protocol is always set to None when
+web_socket_do_extra_handshake is called. If ws_requested_protocols is not
+None, you must choose one subprotocol from this list and set it to
+ws_protocol.
+
+For HyBi 00, when web_socket_do_extra_handshake is called,
+ws_protocol is set to the value given by the client in
+Sec-WebSocket-Protocol header or None if
+such header was not found in the opening handshake request. Finish extra
+handshake with ws_protocol untouched to accept the request subprotocol.
+Then, Sec-WebSocket-Protocol header will be sent to
+the client in response with the same value as requested. Raise an exception
+in web_socket_do_extra_handshake to reject the requested subprotocol.
+
+
+Data Transfer
+-------------
+
+web_socket_transfer_data is called after the handshake completed
+successfully. A handler can receive/send messages from/to the client
+using request. mod_pywebsocket.msgutil module provides utilities
+for data transfer.
+
+You can receive a message by the following statement.
+
+    message = request.ws_stream.receive_message()
+
+This call blocks until any complete text frame arrives, and the payload data
+of the incoming frame will be stored into message. When you're using IETF
+HyBi 00 or later protocol, receive_message() will return None on receiving
+client-initiated closing handshake. When any error occurs, receive_message()
+will raise some exception.
+
+You can send a message by the following statement.
+
+    request.ws_stream.send_message(message)
+
+
+Closing Connection
+------------------
+
+Executing the following statement or just return-ing from
+web_socket_transfer_data cause connection close.
+
+    request.ws_stream.close_connection()
+
+close_connection will wait
+for closing handshake acknowledgement coming from the client. When it
+couldn't receive a valid acknowledgement, raises an exception.
+
+web_socket_passive_closing_handshake is called after the server receives
+incoming closing frame from the client peer immediately. You can specify
+code and reason by return values. They are sent as a outgoing closing frame
+from the server. A request object has the following properties that you can
+use in web_socket_passive_closing_handshake.
+- ws_close_code
+- ws_close_reason
+
+
+Threading
+---------
+
+A WebSocket handler must be thread-safe if the server (Apache or
+standalone.py) is configured to use threads.
+"""
+
+
+# vi:sts=4 sw=4 et tw=72
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/_stream_base.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/_stream_base.py
new file mode 100644
index 0000000..60fb33d
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/_stream_base.py
@@ -0,0 +1,165 @@
+# Copyright 2011, 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.
+
+
+"""Base stream class.
+"""
+
+
+# Note: request.connection.write/read are used in this module, even though
+# mod_python document says that they should be used only in connection
+# handlers. Unfortunately, we have no other options. For example,
+# request.write/read are not suitable because they don't allow direct raw bytes
+# writing/reading.
+
+
+from mod_pywebsocket import util
+
+
+# Exceptions
+
+
+class ConnectionTerminatedException(Exception):
+    """This exception will be raised when a connection is terminated
+    unexpectedly.
+    """
+
+    pass
+
+
+class InvalidFrameException(ConnectionTerminatedException):
+    """This exception will be raised when we received an invalid frame we
+    cannot parse.
+    """
+
+    pass
+
+
+class BadOperationException(Exception):
+    """This exception will be raised when send_message() is called on
+    server-terminated connection or receive_message() is called on
+    client-terminated connection.
+    """
+
+    pass
+
+
+class UnsupportedFrameException(Exception):
+    """This exception will be raised when we receive a frame with flag, opcode
+    we cannot handle. Handlers can just catch and ignore this exception and
+    call receive_message() again to continue processing the next frame.
+    """
+
+    pass
+
+
+class InvalidUTF8Exception(Exception):
+    """This exception will be raised when we receive a text frame which
+    contains invalid UTF-8 strings.
+    """
+
+    pass
+
+
+class StreamBase(object):
+    """Base stream class."""
+
+    def __init__(self, request):
+        """Construct an instance.
+
+        Args:
+            request: mod_python request.
+        """
+
+        self._logger = util.get_class_logger(self)
+
+        self._request = request
+
+    def _read(self, length):
+        """Reads length bytes from connection. In case we catch any exception,
+        prepends remote address to the exception message and raise again.
+
+        Raises:
+            ConnectionTerminatedException: when read returns empty string.
+        """
+
+        bytes = self._request.connection.read(length)
+        if not bytes:
+            raise ConnectionTerminatedException(
+                'Receiving %d byte failed. Peer (%r) closed connection' %
+                (length, (self._request.connection.remote_addr,)))
+        return bytes
+
+    def _write(self, bytes):
+        """Writes given bytes to connection. In case we catch any exception,
+        prepends remote address to the exception message and raise again.
+        """
+
+        try:
+            self._request.connection.write(bytes)
+        except Exception, e:
+            util.prepend_message_to_exception(
+                    'Failed to send message to %r: ' %
+                            (self._request.connection.remote_addr,),
+                    e)
+            raise
+
+    def receive_bytes(self, length):
+        """Receives multiple bytes. Retries read when we couldn't receive the
+        specified amount.
+
+        Raises:
+            ConnectionTerminatedException: when read returns empty string.
+        """
+
+        bytes = []
+        while length > 0:
+            new_bytes = self._read(length)
+            bytes.append(new_bytes)
+            length -= len(new_bytes)
+        return ''.join(bytes)
+
+    def _read_until(self, delim_char):
+        """Reads bytes until we encounter delim_char. The result will not
+        contain delim_char.
+
+        Raises:
+            ConnectionTerminatedException: when read returns empty string.
+        """
+
+        bytes = []
+        while True:
+            ch = self._read(1)
+            if ch == delim_char:
+                break
+            bytes.append(ch)
+        return ''.join(bytes)
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/_stream_hixie75.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/_stream_hixie75.py
new file mode 100644
index 0000000..94cf5b3
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/_stream_hixie75.py
@@ -0,0 +1,229 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""This file provides a class for parsing/building frames of the WebSocket
+protocol version HyBi 00 and Hixie 75.
+
+Specification:
+- HyBi 00 http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00
+- Hixie 75 http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
+"""
+
+
+from mod_pywebsocket import common
+from mod_pywebsocket._stream_base import BadOperationException
+from mod_pywebsocket._stream_base import ConnectionTerminatedException
+from mod_pywebsocket._stream_base import InvalidFrameException
+from mod_pywebsocket._stream_base import StreamBase
+from mod_pywebsocket._stream_base import UnsupportedFrameException
+from mod_pywebsocket import util
+
+
+class StreamHixie75(StreamBase):
+    """A class for parsing/building frames of the WebSocket protocol version
+    HyBi 00 and Hixie 75.
+    """
+
+    def __init__(self, request, enable_closing_handshake=False):
+        """Construct an instance.
+
+        Args:
+            request: mod_python request.
+            enable_closing_handshake: to let StreamHixie75 perform closing
+                                      handshake as specified in HyBi 00, set
+                                      this option to True.
+        """
+
+        StreamBase.__init__(self, request)
+
+        self._logger = util.get_class_logger(self)
+
+        self._enable_closing_handshake = enable_closing_handshake
+
+        self._request.client_terminated = False
+        self._request.server_terminated = False
+
+    def send_message(self, message, end=True, binary=False):
+        """Send message.
+
+        Args:
+            message: unicode string to send.
+            binary: not used in hixie75.
+
+        Raises:
+            BadOperationException: when called on a server-terminated
+                connection.
+        """
+
+        if not end:
+            raise BadOperationException(
+                'StreamHixie75 doesn\'t support send_message with end=False')
+
+        if binary:
+            raise BadOperationException(
+                'StreamHixie75 doesn\'t support send_message with binary=True')
+
+        if self._request.server_terminated:
+            raise BadOperationException(
+                'Requested send_message after sending out a closing handshake')
+
+        self._write(''.join(['\x00', message.encode('utf-8'), '\xff']))
+
+    def _read_payload_length_hixie75(self):
+        """Reads a length header in a Hixie75 version frame with length.
+
+        Raises:
+            ConnectionTerminatedException: when read returns empty string.
+        """
+
+        length = 0
+        while True:
+            b_str = self._read(1)
+            b = ord(b_str)
+            length = length * 128 + (b & 0x7f)
+            if (b & 0x80) == 0:
+                break
+        return length
+
+    def receive_message(self):
+        """Receive a WebSocket frame and return its payload an unicode string.
+
+        Returns:
+            payload unicode string in a WebSocket frame.
+
+        Raises:
+            ConnectionTerminatedException: when read returns empty
+                string.
+            BadOperationException: when called on a client-terminated
+                connection.
+        """
+
+        if self._request.client_terminated:
+            raise BadOperationException(
+                'Requested receive_message after receiving a closing '
+                'handshake')
+
+        while True:
+            # Read 1 byte.
+            # mp_conn.read will block if no bytes are available.
+            # Timeout is controlled by TimeOut directive of Apache.
+            frame_type_str = self.receive_bytes(1)
+            frame_type = ord(frame_type_str)
+            if (frame_type & 0x80) == 0x80:
+                # The payload length is specified in the frame.
+                # Read and discard.
+                length = self._read_payload_length_hixie75()
+                if length > 0:
+                    _ = self.receive_bytes(length)
+                # 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the
+                # /client terminated/ flag and abort these steps.
+                if not self._enable_closing_handshake:
+                    continue
+
+                if frame_type == 0xFF and length == 0:
+                    self._request.client_terminated = True
+
+                    if self._request.server_terminated:
+                        self._logger.debug(
+                            'Received ack for server-initiated closing '
+                            'handshake')
+                        return None
+
+                    self._logger.debug(
+                        'Received client-initiated closing handshake')
+
+                    self._send_closing_handshake()
+                    self._logger.debug(
+                        'Sent ack for client-initiated closing handshake')
+                    return None
+            else:
+                # The payload is delimited with \xff.
+                bytes = self._read_until('\xff')
+                # The WebSocket protocol section 4.4 specifies that invalid
+                # characters must be replaced with U+fffd REPLACEMENT
+                # CHARACTER.
+                message = bytes.decode('utf-8', 'replace')
+                if frame_type == 0x00:
+                    return message
+                # Discard data of other types.
+
+    def _send_closing_handshake(self):
+        if not self._enable_closing_handshake:
+            raise BadOperationException(
+                'Closing handshake is not supported in Hixie 75 protocol')
+
+        self._request.server_terminated = True
+
+        # 5.3 the server may decide to terminate the WebSocket connection by
+        # running through the following steps:
+        # 1. send a 0xFF byte and a 0x00 byte to the client to indicate the
+        # start of the closing handshake.
+        self._write('\xff\x00')
+
+    def close_connection(self, unused_code='', unused_reason=''):
+        """Closes a WebSocket connection.
+
+        Raises:
+            ConnectionTerminatedException: when closing handshake was
+                not successfull.
+        """
+
+        if self._request.server_terminated:
+            self._logger.debug(
+                'Requested close_connection but server is already terminated')
+            return
+
+        if not self._enable_closing_handshake:
+            self._request.server_terminated = True
+            self._logger.debug('Connection closed')
+            return
+
+        self._send_closing_handshake()
+        self._logger.debug('Sent server-initiated closing handshake')
+
+        # TODO(ukai): 2. wait until the /client terminated/ flag has been set,
+        # or until a server-defined timeout expires.
+        #
+        # For now, we expect receiving closing handshake right after sending
+        # out closing handshake, and if we couldn't receive non-handshake
+        # frame, we take it as ConnectionTerminatedException.
+        message = self.receive_message()
+        if message is not None:
+            raise ConnectionTerminatedException(
+                'Didn\'t receive valid ack for closing handshake')
+        # TODO: 3. close the WebSocket connection.
+        # note: mod_python Connection (mp_conn) doesn't have close method.
+
+    def send_ping(self, body):
+        raise BadOperationException(
+            'StreamHixie75 doesn\'t support send_ping')
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/_stream_hybi.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/_stream_hybi.py
new file mode 100644
index 0000000..bd158fa
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/_stream_hybi.py
@@ -0,0 +1,915 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""This file provides classes and helper functions for parsing/building frames
+of the WebSocket protocol (RFC 6455).
+
+Specification:
+http://tools.ietf.org/html/rfc6455
+"""
+
+
+from collections import deque
+import logging
+import os
+import struct
+import time
+
+from mod_pywebsocket import common
+from mod_pywebsocket import util
+from mod_pywebsocket._stream_base import BadOperationException
+from mod_pywebsocket._stream_base import ConnectionTerminatedException
+from mod_pywebsocket._stream_base import InvalidFrameException
+from mod_pywebsocket._stream_base import InvalidUTF8Exception
+from mod_pywebsocket._stream_base import StreamBase
+from mod_pywebsocket._stream_base import UnsupportedFrameException
+
+
+_NOOP_MASKER = util.NoopMasker()
+
+
+class Frame(object):
+
+    def __init__(self, fin=1, rsv1=0, rsv2=0, rsv3=0,
+                 opcode=None, payload=''):
+        self.fin = fin
+        self.rsv1 = rsv1
+        self.rsv2 = rsv2
+        self.rsv3 = rsv3
+        self.opcode = opcode
+        self.payload = payload
+
+
+# Helper functions made public to be used for writing unittests for WebSocket
+# clients.
+
+
+def create_length_header(length, mask):
+    """Creates a length header.
+
+    Args:
+        length: Frame length. Must be less than 2^63.
+        mask: Mask bit. Must be boolean.
+
+    Raises:
+        ValueError: when bad data is given.
+    """
+
+    if mask:
+        mask_bit = 1 << 7
+    else:
+        mask_bit = 0
+
+    if length < 0:
+        raise ValueError('length must be non negative integer')
+    elif length <= 125:
+        return chr(mask_bit | length)
+    elif length < (1 << 16):
+        return chr(mask_bit | 126) + struct.pack('!H', length)
+    elif length < (1 << 63):
+        return chr(mask_bit | 127) + struct.pack('!Q', length)
+    else:
+        raise ValueError('Payload is too big for one frame')
+
+
+def create_header(opcode, payload_length, fin, rsv1, rsv2, rsv3, mask):
+    """Creates a frame header.
+
+    Raises:
+        Exception: when bad data is given.
+    """
+
+    if opcode < 0 or 0xf < opcode:
+        raise ValueError('Opcode out of range')
+
+    if payload_length < 0 or (1 << 63) <= payload_length:
+        raise ValueError('payload_length out of range')
+
+    if (fin | rsv1 | rsv2 | rsv3) & ~1:
+        raise ValueError('FIN bit and Reserved bit parameter must be 0 or 1')
+
+    header = ''
+
+    first_byte = ((fin << 7)
+                  | (rsv1 << 6) | (rsv2 << 5) | (rsv3 << 4)
+                  | opcode)
+    header += chr(first_byte)
+    header += create_length_header(payload_length, mask)
+
+    return header
+
+
+def _build_frame(header, body, mask):
+    if not mask:
+        return header + body
+
+    masking_nonce = os.urandom(4)
+    masker = util.RepeatedXorMasker(masking_nonce)
+
+    return header + masking_nonce + masker.mask(body)
+
+
+def _filter_and_format_frame_object(frame, mask, frame_filters):
+    for frame_filter in frame_filters:
+        frame_filter.filter(frame)
+
+    header = create_header(
+        frame.opcode, len(frame.payload), frame.fin,
+        frame.rsv1, frame.rsv2, frame.rsv3, mask)
+    return _build_frame(header, frame.payload, mask)
+
+
+def create_binary_frame(
+    message, opcode=common.OPCODE_BINARY, fin=1, mask=False, frame_filters=[]):
+    """Creates a simple binary frame with no extension, reserved bit."""
+
+    frame = Frame(fin=fin, opcode=opcode, payload=message)
+    return _filter_and_format_frame_object(frame, mask, frame_filters)
+
+
+def create_text_frame(
+    message, opcode=common.OPCODE_TEXT, fin=1, mask=False, frame_filters=[]):
+    """Creates a simple text frame with no extension, reserved bit."""
+
+    encoded_message = message.encode('utf-8')
+    return create_binary_frame(encoded_message, opcode, fin, mask,
+                               frame_filters)
+
+
+def parse_frame(receive_bytes, logger=None,
+                ws_version=common.VERSION_HYBI_LATEST,
+                unmask_receive=True):
+    """Parses a frame. Returns a tuple containing each header field and
+    payload.
+
+    Args:
+        receive_bytes: a function that reads frame data from a stream or
+            something similar. The function takes length of the bytes to be
+            read. The function must raise ConnectionTerminatedException if
+            there is not enough data to be read.
+        logger: a logging object.
+        ws_version: the version of WebSocket protocol.
+        unmask_receive: unmask received frames. When received unmasked
+            frame, raises InvalidFrameException.
+
+    Raises:
+        ConnectionTerminatedException: when receive_bytes raises it.
+        InvalidFrameException: when the frame contains invalid data.
+    """
+
+    if not logger:
+        logger = logging.getLogger()
+
+    logger.log(common.LOGLEVEL_FINE, 'Receive the first 2 octets of a frame')
+
+    received = receive_bytes(2)
+
+    first_byte = ord(received[0])
+    fin = (first_byte >> 7) & 1
+    rsv1 = (first_byte >> 6) & 1
+    rsv2 = (first_byte >> 5) & 1
+    rsv3 = (first_byte >> 4) & 1
+    opcode = first_byte & 0xf
+
+    second_byte = ord(received[1])
+    mask = (second_byte >> 7) & 1
+    payload_length = second_byte & 0x7f
+
+    logger.log(common.LOGLEVEL_FINE,
+               'FIN=%s, RSV1=%s, RSV2=%s, RSV3=%s, opcode=%s, '
+               'Mask=%s, Payload_length=%s',
+               fin, rsv1, rsv2, rsv3, opcode, mask, payload_length)
+
+    if (mask == 1) != unmask_receive:
+        raise InvalidFrameException(
+            'Mask bit on the received frame did\'nt match masking '
+            'configuration for received frames')
+
+    # The HyBi and later specs disallow putting a value in 0x0-0xFFFF
+    # into the 8-octet extended payload length field (or 0x0-0xFD in
+    # 2-octet field).
+    valid_length_encoding = True
+    length_encoding_bytes = 1
+    if payload_length == 127:
+        logger.log(common.LOGLEVEL_FINE,
+                   'Receive 8-octet extended payload length')
+
+        extended_payload_length = receive_bytes(8)
+        payload_length = struct.unpack(
+            '!Q', extended_payload_length)[0]
+        if payload_length > 0x7FFFFFFFFFFFFFFF:
+            raise InvalidFrameException(
+                'Extended payload length >= 2^63')
+        if ws_version >= 13 and payload_length < 0x10000:
+            valid_length_encoding = False
+            length_encoding_bytes = 8
+
+        logger.log(common.LOGLEVEL_FINE,
+                   'Decoded_payload_length=%s', payload_length)
+    elif payload_length == 126:
+        logger.log(common.LOGLEVEL_FINE,
+                   'Receive 2-octet extended payload length')
+
+        extended_payload_length = receive_bytes(2)
+        payload_length = struct.unpack(
+            '!H', extended_payload_length)[0]
+        if ws_version >= 13 and payload_length < 126:
+            valid_length_encoding = False
+            length_encoding_bytes = 2
+
+        logger.log(common.LOGLEVEL_FINE,
+                   'Decoded_payload_length=%s', payload_length)
+
+    if not valid_length_encoding:
+        logger.warning(
+            'Payload length is not encoded using the minimal number of '
+            'bytes (%d is encoded using %d bytes)',
+            payload_length,
+            length_encoding_bytes)
+
+    if mask == 1:
+        logger.log(common.LOGLEVEL_FINE, 'Receive mask')
+
+        masking_nonce = receive_bytes(4)
+        masker = util.RepeatedXorMasker(masking_nonce)
+
+        logger.log(common.LOGLEVEL_FINE, 'Mask=%r', masking_nonce)
+    else:
+        masker = _NOOP_MASKER
+
+    logger.log(common.LOGLEVEL_FINE, 'Receive payload data')
+    if logger.isEnabledFor(common.LOGLEVEL_FINE):
+        receive_start = time.time()
+
+    raw_payload_bytes = receive_bytes(payload_length)
+
+    if logger.isEnabledFor(common.LOGLEVEL_FINE):
+        logger.log(
+            common.LOGLEVEL_FINE,
+            'Done receiving payload data at %s MB/s',
+            payload_length / (time.time() - receive_start) / 1000 / 1000)
+    logger.log(common.LOGLEVEL_FINE, 'Unmask payload data')
+
+    if logger.isEnabledFor(common.LOGLEVEL_FINE):
+        unmask_start = time.time()
+
+    bytes = masker.mask(raw_payload_bytes)
+
+    if logger.isEnabledFor(common.LOGLEVEL_FINE):
+        logger.log(
+            common.LOGLEVEL_FINE,
+            'Done unmasking payload data at %s MB/s',
+            payload_length / (time.time() - unmask_start) / 1000 / 1000)
+
+    return opcode, bytes, fin, rsv1, rsv2, rsv3
+
+
+class FragmentedFrameBuilder(object):
+    """A stateful class to send a message as fragments."""
+
+    def __init__(self, mask, frame_filters=[], encode_utf8=True):
+        """Constructs an instance."""
+
+        self._mask = mask
+        self._frame_filters = frame_filters
+        # This is for skipping UTF-8 encoding when building text type frames
+        # from compressed data.
+        self._encode_utf8 = encode_utf8
+
+        self._started = False
+
+        # Hold opcode of the first frame in messages to verify types of other
+        # frames in the message are all the same.
+        self._opcode = common.OPCODE_TEXT
+
+    def build(self, payload_data, end, binary):
+        if binary:
+            frame_type = common.OPCODE_BINARY
+        else:
+            frame_type = common.OPCODE_TEXT
+        if self._started:
+            if self._opcode != frame_type:
+                raise ValueError('Message types are different in frames for '
+                                 'the same message')
+            opcode = common.OPCODE_CONTINUATION
+        else:
+            opcode = frame_type
+            self._opcode = frame_type
+
+        if end:
+            self._started = False
+            fin = 1
+        else:
+            self._started = True
+            fin = 0
+
+        if binary or not self._encode_utf8:
+            return create_binary_frame(
+                payload_data, opcode, fin, self._mask, self._frame_filters)
+        else:
+            return create_text_frame(
+                payload_data, opcode, fin, self._mask, self._frame_filters)
+
+
+def _create_control_frame(opcode, body, mask, frame_filters):
+    frame = Frame(opcode=opcode, payload=body)
+
+    for frame_filter in frame_filters:
+        frame_filter.filter(frame)
+
+    if len(frame.payload) > 125:
+        raise BadOperationException(
+            'Payload data size of control frames must be 125 bytes or less')
+
+    header = create_header(
+        frame.opcode, len(frame.payload), frame.fin,
+        frame.rsv1, frame.rsv2, frame.rsv3, mask)
+    return _build_frame(header, frame.payload, mask)
+
+
+def create_ping_frame(body, mask=False, frame_filters=[]):
+    return _create_control_frame(common.OPCODE_PING, body, mask, frame_filters)
+
+
+def create_pong_frame(body, mask=False, frame_filters=[]):
+    return _create_control_frame(common.OPCODE_PONG, body, mask, frame_filters)
+
+
+def create_close_frame(body, mask=False, frame_filters=[]):
+    return _create_control_frame(
+        common.OPCODE_CLOSE, body, mask, frame_filters)
+
+
+def create_closing_handshake_body(code, reason):
+    body = ''
+    if code is not None:
+        if (code > common.STATUS_USER_PRIVATE_MAX or
+            code < common.STATUS_NORMAL_CLOSURE):
+            raise BadOperationException('Status code is out of range')
+        if (code == common.STATUS_NO_STATUS_RECEIVED or
+            code == common.STATUS_ABNORMAL_CLOSURE or
+            code == common.STATUS_TLS_HANDSHAKE):
+            raise BadOperationException('Status code is reserved pseudo '
+                'code')
+        encoded_reason = reason.encode('utf-8')
+        body = struct.pack('!H', code) + encoded_reason
+    return body
+
+
+class StreamOptions(object):
+    """Holds option values to configure Stream objects."""
+
+    def __init__(self):
+        """Constructs StreamOptions."""
+
+        # Enables deflate-stream extension.
+        self.deflate_stream = False
+
+        # Filters applied to frames.
+        self.outgoing_frame_filters = []
+        self.incoming_frame_filters = []
+
+        # Filters applied to messages. Control frames are not affected by them.
+        self.outgoing_message_filters = []
+        self.incoming_message_filters = []
+
+        self.encode_text_message_to_utf8 = True
+        self.mask_send = False
+        self.unmask_receive = True
+        # RFC6455 disallows fragmented control frames, but mux extension
+        # relaxes the restriction.
+        self.allow_fragmented_control_frame = False
+
+
+class Stream(StreamBase):
+    """A class for parsing/building frames of the WebSocket protocol
+    (RFC 6455).
+    """
+
+    def __init__(self, request, options):
+        """Constructs an instance.
+
+        Args:
+            request: mod_python request.
+        """
+
+        StreamBase.__init__(self, request)
+
+        self._logger = util.get_class_logger(self)
+
+        self._options = options
+
+        if self._options.deflate_stream:
+            self._logger.debug('Setup filter for deflate-stream')
+            self._request = util.DeflateRequest(self._request)
+
+        self._request.client_terminated = False
+        self._request.server_terminated = False
+
+        # Holds body of received fragments.
+        self._received_fragments = []
+        # Holds the opcode of the first fragment.
+        self._original_opcode = None
+
+        self._writer = FragmentedFrameBuilder(
+            self._options.mask_send, self._options.outgoing_frame_filters,
+            self._options.encode_text_message_to_utf8)
+
+        self._ping_queue = deque()
+
+    def _receive_frame(self):
+        """Receives a frame and return data in the frame as a tuple containing
+        each header field and payload separately.
+
+        Raises:
+            ConnectionTerminatedException: when read returns empty
+                string.
+            InvalidFrameException: when the frame contains invalid data.
+        """
+
+        def _receive_bytes(length):
+            return self.receive_bytes(length)
+
+        return parse_frame(receive_bytes=_receive_bytes,
+                           logger=self._logger,
+                           ws_version=self._request.ws_version,
+                           unmask_receive=self._options.unmask_receive)
+
+    def _receive_frame_as_frame_object(self):
+        opcode, bytes, fin, rsv1, rsv2, rsv3 = self._receive_frame()
+
+        return Frame(fin=fin, rsv1=rsv1, rsv2=rsv2, rsv3=rsv3,
+                     opcode=opcode, payload=bytes)
+
+    def receive_filtered_frame(self):
+        """Receives a frame and applies frame filters and message filters.
+        The frame to be received must satisfy following conditions:
+        - The frame is not fragmented.
+        - The opcode of the frame is TEXT or BINARY.
+
+        DO NOT USE this method except for testing purpose.
+        """
+
+        frame = self._receive_frame_as_frame_object()
+        if not frame.fin:
+            raise InvalidFrameException(
+                'Segmented frames must not be received via '
+                'receive_filtered_frame()')
+        if (frame.opcode != common.OPCODE_TEXT and
+            frame.opcode != common.OPCODE_BINARY):
+            raise InvalidFrameException(
+                'Control frames must not be received via '
+                'receive_filtered_frame()')
+
+        for frame_filter in self._options.incoming_frame_filters:
+            frame_filter.filter(frame)
+        for message_filter in self._options.incoming_message_filters:
+            frame.payload = message_filter.filter(frame.payload)
+        return frame
+
+    def send_message(self, message, end=True, binary=False):
+        """Send message.
+
+        Args:
+            message: text in unicode or binary in str to send.
+            binary: send message as binary frame.
+
+        Raises:
+            BadOperationException: when called on a server-terminated
+                connection or called with inconsistent message type or
+                binary parameter.
+        """
+
+        if self._request.server_terminated:
+            raise BadOperationException(
+                'Requested send_message after sending out a closing handshake')
+
+        if binary and isinstance(message, unicode):
+            raise BadOperationException(
+                'Message for binary frame must be instance of str')
+
+        for message_filter in self._options.outgoing_message_filters:
+            message = message_filter.filter(message, end, binary)
+
+        try:
+            # Set this to any positive integer to limit maximum size of data in
+            # payload data of each frame.
+            MAX_PAYLOAD_DATA_SIZE = -1
+
+            if MAX_PAYLOAD_DATA_SIZE <= 0:
+                self._write(self._writer.build(message, end, binary))
+                return
+
+            bytes_written = 0
+            while True:
+                end_for_this_frame = end
+                bytes_to_write = len(message) - bytes_written
+                if (MAX_PAYLOAD_DATA_SIZE > 0 and
+                    bytes_to_write > MAX_PAYLOAD_DATA_SIZE):
+                    end_for_this_frame = False
+                    bytes_to_write = MAX_PAYLOAD_DATA_SIZE
+
+                frame = self._writer.build(
+                    message[bytes_written:bytes_written + bytes_to_write],
+                    end_for_this_frame,
+                    binary)
+                self._write(frame)
+
+                bytes_written += bytes_to_write
+
+                # This if must be placed here (the end of while block) so that
+                # at least one frame is sent.
+                if len(message) <= bytes_written:
+                    break
+        except ValueError, e:
+            raise BadOperationException(e)
+
+    def _get_message_from_frame(self, frame):
+        """Gets a message from frame. If the message is composed of fragmented
+        frames and the frame is not the last fragmented frame, this method
+        returns None. The whole message will be returned when the last
+        fragmented frame is passed to this method.
+
+        Raises:
+            InvalidFrameException: when the frame doesn't match defragmentation
+                context, or the frame contains invalid data.
+        """
+
+        if frame.opcode == common.OPCODE_CONTINUATION:
+            if not self._received_fragments:
+                if frame.fin:
+                    raise InvalidFrameException(
+                        'Received a termination frame but fragmentation '
+                        'not started')
+                else:
+                    raise InvalidFrameException(
+                        'Received an intermediate frame but '
+                        'fragmentation not started')
+
+            if frame.fin:
+                # End of fragmentation frame
+                self._received_fragments.append(frame.payload)
+                message = ''.join(self._received_fragments)
+                self._received_fragments = []
+                return message
+            else:
+                # Intermediate frame
+                self._received_fragments.append(frame.payload)
+                return None
+        else:
+            if self._received_fragments:
+                if frame.fin:
+                    raise InvalidFrameException(
+                        'Received an unfragmented frame without '
+                        'terminating existing fragmentation')
+                else:
+                    raise InvalidFrameException(
+                        'New fragmentation started without terminating '
+                        'existing fragmentation')
+
+            if frame.fin:
+                # Unfragmented frame
+
+                self._original_opcode = frame.opcode
+                return frame.payload
+            else:
+                # Start of fragmentation frame
+
+                if (not self._options.allow_fragmented_control_frame and
+                    common.is_control_opcode(frame.opcode)):
+                    raise InvalidFrameException(
+                        'Control frames must not be fragmented')
+
+                self._original_opcode = frame.opcode
+                self._received_fragments.append(frame.payload)
+                return None
+
+    def _process_close_message(self, message):
+        """Processes close message.
+
+        Args:
+            message: close message.
+
+        Raises:
+            InvalidFrameException: when the message is invalid.
+        """
+
+        self._request.client_terminated = True
+
+        # Status code is optional. We can have status reason only if we
+        # have status code. Status reason can be empty string. So,
+        # allowed cases are
+        # - no application data: no code no reason
+        # - 2 octet of application data: has code but no reason
+        # - 3 or more octet of application data: both code and reason
+        if len(message) == 0:
+            self._logger.debug('Received close frame (empty body)')
+            self._request.ws_close_code = (
+                common.STATUS_NO_STATUS_RECEIVED)
+        elif len(message) == 1:
+            raise InvalidFrameException(
+                'If a close frame has status code, the length of '
+                'status code must be 2 octet')
+        elif len(message) >= 2:
+            self._request.ws_close_code = struct.unpack(
+                '!H', message[0:2])[0]
+            self._request.ws_close_reason = message[2:].decode(
+                'utf-8', 'replace')
+            self._logger.debug(
+                'Received close frame (code=%d, reason=%r)',
+                self._request.ws_close_code,
+                self._request.ws_close_reason)
+
+        # Drain junk data after the close frame if necessary.
+        self._drain_received_data()
+
+        if self._request.server_terminated:
+            self._logger.debug(
+                'Received ack for server-initiated closing handshake')
+            return
+
+        self._logger.debug(
+            'Received client-initiated closing handshake')
+
+        code = common.STATUS_NORMAL_CLOSURE
+        reason = ''
+        if hasattr(self._request, '_dispatcher'):
+            dispatcher = self._request._dispatcher
+            code, reason = dispatcher.passive_closing_handshake(
+                self._request)
+            if code is None and reason is not None and len(reason) > 0:
+                self._logger.warning(
+                    'Handler specified reason despite code being None')
+                reason = ''
+            if reason is None:
+                reason = ''
+        self._send_closing_handshake(code, reason)
+        self._logger.debug(
+            'Sent ack for client-initiated closing handshake '
+            '(code=%r, reason=%r)', code, reason)
+
+    def _process_ping_message(self, message):
+        """Processes ping message.
+
+        Args:
+            message: ping message.
+        """
+
+        try:
+            handler = self._request.on_ping_handler
+            if handler:
+                handler(self._request, message)
+                return
+        except AttributeError, e:
+            pass
+        self._send_pong(message)
+
+    def _process_pong_message(self, message):
+        """Processes pong message.
+
+        Args:
+            message: pong message.
+        """
+
+        # TODO(tyoshino): Add ping timeout handling.
+
+        inflight_pings = deque()
+
+        while True:
+            try:
+                expected_body = self._ping_queue.popleft()
+                if expected_body == message:
+                    # inflight_pings contains pings ignored by the
+                    # other peer. Just forget them.
+                    self._logger.debug(
+                        'Ping %r is acked (%d pings were ignored)',
+                        expected_body, len(inflight_pings))
+                    break
+                else:
+                    inflight_pings.append(expected_body)
+            except IndexError, e:
+                # The received pong was unsolicited pong. Keep the
+                # ping queue as is.
+                self._ping_queue = inflight_pings
+                self._logger.debug('Received a unsolicited pong')
+                break
+
+        try:
+            handler = self._request.on_pong_handler
+            if handler:
+                handler(self._request, message)
+        except AttributeError, e:
+            pass
+
+    def receive_message(self):
+        """Receive a WebSocket frame and return its payload as a text in
+        unicode or a binary in str.
+
+        Returns:
+            payload data of the frame
+            - as unicode instance if received text frame
+            - as str instance if received binary frame
+            or None iff received closing handshake.
+        Raises:
+            BadOperationException: when called on a client-terminated
+                connection.
+            ConnectionTerminatedException: when read returns empty
+                string.
+            InvalidFrameException: when the frame contains invalid
+                data.
+            UnsupportedFrameException: when the received frame has
+                flags, opcode we cannot handle. You can ignore this
+                exception and continue receiving the next frame.
+        """
+
+        if self._request.client_terminated:
+            raise BadOperationException(
+                'Requested receive_message after receiving a closing '
+                'handshake')
+
+        while True:
+            # mp_conn.read will block if no bytes are available.
+            # Timeout is controlled by TimeOut directive of Apache.
+
+            frame = self._receive_frame_as_frame_object()
+
+            # Check the constraint on the payload size for control frames
+            # before extension processes the frame.
+            # See also http://tools.ietf.org/html/rfc6455#section-5.5
+            if (common.is_control_opcode(frame.opcode) and
+                len(frame.payload) > 125):
+                raise InvalidFrameException(
+                    'Payload data size of control frames must be 125 bytes or '
+                    'less')
+
+            for frame_filter in self._options.incoming_frame_filters:
+                frame_filter.filter(frame)
+
+            if frame.rsv1 or frame.rsv2 or frame.rsv3:
+                raise UnsupportedFrameException(
+                    'Unsupported flag is set (rsv = %d%d%d)' %
+                    (frame.rsv1, frame.rsv2, frame.rsv3))
+
+            message = self._get_message_from_frame(frame)
+            if message is None:
+                continue
+
+            for message_filter in self._options.incoming_message_filters:
+                message = message_filter.filter(message)
+
+            if self._original_opcode == common.OPCODE_TEXT:
+                # The WebSocket protocol section 4.4 specifies that invalid
+                # characters must be replaced with U+fffd REPLACEMENT
+                # CHARACTER.
+                try:
+                    return message.decode('utf-8')
+                except UnicodeDecodeError, e:
+                    raise InvalidUTF8Exception(e)
+            elif self._original_opcode == common.OPCODE_BINARY:
+                return message
+            elif self._original_opcode == common.OPCODE_CLOSE:
+                self._process_close_message(message)
+                return None
+            elif self._original_opcode == common.OPCODE_PING:
+                self._process_ping_message(message)
+            elif self._original_opcode == common.OPCODE_PONG:
+                self._process_pong_message(message)
+            else:
+                raise UnsupportedFrameException(
+                    'Opcode %d is not supported' % self._original_opcode)
+
+    def _send_closing_handshake(self, code, reason):
+        body = create_closing_handshake_body(code, reason)
+        frame = create_close_frame(
+            body, mask=self._options.mask_send,
+            frame_filters=self._options.outgoing_frame_filters)
+
+        self._request.server_terminated = True
+
+        self._write(frame)
+
+    def close_connection(self, code=common.STATUS_NORMAL_CLOSURE, reason=''):
+        """Closes a WebSocket connection.
+
+        Args:
+            code: Status code for close frame. If code is None, a close
+                frame with empty body will be sent.
+            reason: string representing close reason.
+        Raises:
+            BadOperationException: when reason is specified with code None
+            or reason is not an instance of both str and unicode.
+        """
+
+        if self._request.server_terminated:
+            self._logger.debug(
+                'Requested close_connection but server is already terminated')
+            return
+
+        if code is None:
+            if reason is not None and len(reason) > 0:
+                raise BadOperationException(
+                    'close reason must not be specified if code is None')
+            reason = ''
+        else:
+            if not isinstance(reason, str) and not isinstance(reason, unicode):
+                raise BadOperationException(
+                    'close reason must be an instance of str or unicode')
+
+        self._send_closing_handshake(code, reason)
+        self._logger.debug(
+            'Sent server-initiated closing handshake (code=%r, reason=%r)',
+            code, reason)
+
+        if (code == common.STATUS_GOING_AWAY or
+            code == common.STATUS_PROTOCOL_ERROR):
+            # It doesn't make sense to wait for a close frame if the reason is
+            # protocol error or that the server is going away. For some of
+            # other reasons, it might not make sense to wait for a close frame,
+            # but it's not clear, yet.
+            return
+
+        # TODO(ukai): 2. wait until the /client terminated/ flag has been set,
+        # or until a server-defined timeout expires.
+        #
+        # For now, we expect receiving closing handshake right after sending
+        # out closing handshake.
+        message = self.receive_message()
+        if message is not None:
+            raise ConnectionTerminatedException(
+                'Didn\'t receive valid ack for closing handshake')
+        # TODO: 3. close the WebSocket connection.
+        # note: mod_python Connection (mp_conn) doesn't have close method.
+
+    def send_ping(self, body=''):
+        frame = create_ping_frame(
+            body,
+            self._options.mask_send,
+            self._options.outgoing_frame_filters)
+        self._write(frame)
+
+        self._ping_queue.append(body)
+
+    def _send_pong(self, body):
+        frame = create_pong_frame(
+            body,
+            self._options.mask_send,
+            self._options.outgoing_frame_filters)
+        self._write(frame)
+
+    def get_last_received_opcode(self):
+        """Returns the opcode of the WebSocket message which the last received
+        frame belongs to. The return value is valid iff immediately after
+        receive_message call.
+        """
+
+        return self._original_opcode
+
+    def _drain_received_data(self):
+        """Drains unread data in the receive buffer to avoid sending out TCP
+        RST packet. This is because when deflate-stream is enabled, some
+        DEFLATE block for flushing data may follow a close frame. If any data
+        remains in the receive buffer of a socket when the socket is closed,
+        it sends out TCP RST packet to the other peer.
+
+        Since mod_python's mp_conn object doesn't support non-blocking read,
+        we perform this only when pywebsocket is running in standalone mode.
+        """
+
+        # If self._options.deflate_stream is true, self._request is
+        # DeflateRequest, so we can get wrapped request object by
+        # self._request._request.
+        #
+        # Only _StandaloneRequest has _drain_received_data method.
+        if (self._options.deflate_stream and
+            ('_drain_received_data' in dir(self._request._request))):
+            self._request._request._drain_received_data()
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/common.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/common.py
new file mode 100644
index 0000000..2388379
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/common.py
@@ -0,0 +1,307 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""This file must not depend on any module specific to the WebSocket protocol.
+"""
+
+
+from mod_pywebsocket import http_header_util
+
+
+# Additional log level definitions.
+LOGLEVEL_FINE = 9
+
+# Constants indicating WebSocket protocol version.
+VERSION_HIXIE75 = -1
+VERSION_HYBI00 = 0
+VERSION_HYBI01 = 1
+VERSION_HYBI02 = 2
+VERSION_HYBI03 = 2
+VERSION_HYBI04 = 4
+VERSION_HYBI05 = 5
+VERSION_HYBI06 = 6
+VERSION_HYBI07 = 7
+VERSION_HYBI08 = 8
+VERSION_HYBI09 = 8
+VERSION_HYBI10 = 8
+VERSION_HYBI11 = 8
+VERSION_HYBI12 = 8
+VERSION_HYBI13 = 13
+VERSION_HYBI14 = 13
+VERSION_HYBI15 = 13
+VERSION_HYBI16 = 13
+VERSION_HYBI17 = 13
+
+# Constants indicating WebSocket protocol latest version.
+VERSION_HYBI_LATEST = VERSION_HYBI13
+
+# Port numbers
+DEFAULT_WEB_SOCKET_PORT = 80
+DEFAULT_WEB_SOCKET_SECURE_PORT = 443
+
+# Schemes
+WEB_SOCKET_SCHEME = 'ws'
+WEB_SOCKET_SECURE_SCHEME = 'wss'
+
+# Frame opcodes defined in the spec.
+OPCODE_CONTINUATION = 0x0
+OPCODE_TEXT = 0x1
+OPCODE_BINARY = 0x2
+OPCODE_CLOSE = 0x8
+OPCODE_PING = 0x9
+OPCODE_PONG = 0xa
+
+# UUIDs used by HyBi 04 and later opening handshake and frame masking.
+WEBSOCKET_ACCEPT_UUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
+
+# Opening handshake header names and expected values.
+UPGRADE_HEADER = 'Upgrade'
+WEBSOCKET_UPGRADE_TYPE = 'websocket'
+WEBSOCKET_UPGRADE_TYPE_HIXIE75 = 'WebSocket'
+CONNECTION_HEADER = 'Connection'
+UPGRADE_CONNECTION_TYPE = 'Upgrade'
+HOST_HEADER = 'Host'
+ORIGIN_HEADER = 'Origin'
+SEC_WEBSOCKET_ORIGIN_HEADER = 'Sec-WebSocket-Origin'
+SEC_WEBSOCKET_KEY_HEADER = 'Sec-WebSocket-Key'
+SEC_WEBSOCKET_ACCEPT_HEADER = 'Sec-WebSocket-Accept'
+SEC_WEBSOCKET_VERSION_HEADER = 'Sec-WebSocket-Version'
+SEC_WEBSOCKET_PROTOCOL_HEADER = 'Sec-WebSocket-Protocol'
+SEC_WEBSOCKET_EXTENSIONS_HEADER = 'Sec-WebSocket-Extensions'
+SEC_WEBSOCKET_DRAFT_HEADER = 'Sec-WebSocket-Draft'
+SEC_WEBSOCKET_KEY1_HEADER = 'Sec-WebSocket-Key1'
+SEC_WEBSOCKET_KEY2_HEADER = 'Sec-WebSocket-Key2'
+SEC_WEBSOCKET_LOCATION_HEADER = 'Sec-WebSocket-Location'
+
+# Extensions
+DEFLATE_STREAM_EXTENSION = 'deflate-stream'
+DEFLATE_FRAME_EXTENSION = 'deflate-frame'
+PERFRAME_COMPRESSION_EXTENSION = 'perframe-compress'
+PERMESSAGE_COMPRESSION_EXTENSION = 'permessage-compress'
+X_WEBKIT_DEFLATE_FRAME_EXTENSION = 'x-webkit-deflate-frame'
+X_WEBKIT_PERMESSAGE_COMPRESSION_EXTENSION = 'x-webkit-permessage-compress'
+MUX_EXTENSION = 'mux_DO_NOT_USE'
+
+# Status codes
+# Code STATUS_NO_STATUS_RECEIVED, STATUS_ABNORMAL_CLOSURE, and
+# STATUS_TLS_HANDSHAKE are pseudo codes to indicate specific error cases.
+# Could not be used for codes in actual closing frames.
+# Application level errors must use codes in the range
+# STATUS_USER_REGISTERED_BASE to STATUS_USER_PRIVATE_MAX. The codes in the
+# range STATUS_USER_REGISTERED_BASE to STATUS_USER_REGISTERED_MAX are managed
+# by IANA. Usually application must define user protocol level errors in the
+# range STATUS_USER_PRIVATE_BASE to STATUS_USER_PRIVATE_MAX.
+STATUS_NORMAL_CLOSURE = 1000
+STATUS_GOING_AWAY = 1001
+STATUS_PROTOCOL_ERROR = 1002
+STATUS_UNSUPPORTED_DATA = 1003
+STATUS_NO_STATUS_RECEIVED = 1005
+STATUS_ABNORMAL_CLOSURE = 1006
+STATUS_INVALID_FRAME_PAYLOAD_DATA = 1007
+STATUS_POLICY_VIOLATION = 1008
+STATUS_MESSAGE_TOO_BIG = 1009
+STATUS_MANDATORY_EXTENSION = 1010
+STATUS_INTERNAL_ENDPOINT_ERROR = 1011
+STATUS_TLS_HANDSHAKE = 1015
+STATUS_USER_REGISTERED_BASE = 3000
+STATUS_USER_REGISTERED_MAX = 3999
+STATUS_USER_PRIVATE_BASE = 4000
+STATUS_USER_PRIVATE_MAX = 4999
+# Following definitions are aliases to keep compatibility. Applications must
+# not use these obsoleted definitions anymore.
+STATUS_NORMAL = STATUS_NORMAL_CLOSURE
+STATUS_UNSUPPORTED = STATUS_UNSUPPORTED_DATA
+STATUS_CODE_NOT_AVAILABLE = STATUS_NO_STATUS_RECEIVED
+STATUS_ABNORMAL_CLOSE = STATUS_ABNORMAL_CLOSURE
+STATUS_INVALID_FRAME_PAYLOAD = STATUS_INVALID_FRAME_PAYLOAD_DATA
+STATUS_MANDATORY_EXT = STATUS_MANDATORY_EXTENSION
+
+# HTTP status codes
+HTTP_STATUS_BAD_REQUEST = 400
+HTTP_STATUS_FORBIDDEN = 403
+HTTP_STATUS_NOT_FOUND = 404
+
+
+def is_control_opcode(opcode):
+    return (opcode >> 3) == 1
+
+
+class ExtensionParameter(object):
+    """Holds information about an extension which is exchanged on extension
+    negotiation in opening handshake.
+    """
+
+    def __init__(self, name):
+        self._name = name
+        # TODO(tyoshino): Change the data structure to more efficient one such
+        # as dict when the spec changes to say like
+        # - Parameter names must be unique
+        # - The order of parameters is not significant
+        self._parameters = []
+
+    def name(self):
+        return self._name
+
+    def add_parameter(self, name, value):
+        self._parameters.append((name, value))
+
+    def get_parameters(self):
+        return self._parameters
+
+    def get_parameter_names(self):
+        return [name for name, unused_value in self._parameters]
+
+    def has_parameter(self, name):
+        for param_name, param_value in self._parameters:
+            if param_name == name:
+                return True
+        return False
+
+    def get_parameter_value(self, name):
+        for param_name, param_value in self._parameters:
+            if param_name == name:
+                return param_value
+
+
+class ExtensionParsingException(Exception):
+    def __init__(self, name):
+        super(ExtensionParsingException, self).__init__(name)
+
+
+def _parse_extension_param(state, definition, allow_quoted_string):
+    param_name = http_header_util.consume_token(state)
+
+    if param_name is None:
+        raise ExtensionParsingException('No valid parameter name found')
+
+    http_header_util.consume_lwses(state)
+
+    if not http_header_util.consume_string(state, '='):
+        definition.add_parameter(param_name, None)
+        return
+
+    http_header_util.consume_lwses(state)
+
+    if allow_quoted_string:
+        # TODO(toyoshim): Add code to validate that parsed param_value is token
+        param_value = http_header_util.consume_token_or_quoted_string(state)
+    else:
+        param_value = http_header_util.consume_token(state)
+    if param_value is None:
+        raise ExtensionParsingException(
+            'No valid parameter value found on the right-hand side of '
+            'parameter %r' % param_name)
+
+    definition.add_parameter(param_name, param_value)
+
+
+def _parse_extension(state, allow_quoted_string):
+    extension_token = http_header_util.consume_token(state)
+    if extension_token is None:
+        return None
+
+    extension = ExtensionParameter(extension_token)
+
+    while True:
+        http_header_util.consume_lwses(state)
+
+        if not http_header_util.consume_string(state, ';'):
+            break
+
+        http_header_util.consume_lwses(state)
+
+        try:
+            _parse_extension_param(state, extension, allow_quoted_string)
+        except ExtensionParsingException, e:
+            raise ExtensionParsingException(
+                'Failed to parse parameter for %r (%r)' %
+                (extension_token, e))
+
+    return extension
+
+
+def parse_extensions(data, allow_quoted_string=False):
+    """Parses Sec-WebSocket-Extensions header value returns a list of
+    ExtensionParameter objects.
+
+    Leading LWSes must be trimmed.
+    """
+
+    state = http_header_util.ParsingState(data)
+
+    extension_list = []
+    while True:
+        extension = _parse_extension(state, allow_quoted_string)
+        if extension is not None:
+            extension_list.append(extension)
+
+        http_header_util.consume_lwses(state)
+
+        if http_header_util.peek(state) is None:
+            break
+
+        if not http_header_util.consume_string(state, ','):
+            raise ExtensionParsingException(
+                'Failed to parse Sec-WebSocket-Extensions header: '
+                'Expected a comma but found %r' %
+                http_header_util.peek(state))
+
+        http_header_util.consume_lwses(state)
+
+    if len(extension_list) == 0:
+        raise ExtensionParsingException(
+            'No valid extension entry found')
+
+    return extension_list
+
+
+def format_extension(extension):
+    """Formats an ExtensionParameter object."""
+
+    formatted_params = [extension.name()]
+    for param_name, param_value in extension.get_parameters():
+        if param_value is None:
+            formatted_params.append(param_name)
+        else:
+            quoted_value = http_header_util.quote_if_necessary(param_value)
+            formatted_params.append('%s=%s' % (param_name, quoted_value))
+    return '; '.join(formatted_params)
+
+
+def format_extensions(extension_list):
+    """Formats a list of ExtensionParameter objects."""
+
+    formatted_extension_list = []
+    for extension in extension_list:
+        formatted_extension_list.append(format_extension(extension))
+    return ', '.join(formatted_extension_list)
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/dispatch.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/dispatch.py
new file mode 100644
index 0000000..25905f1
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/dispatch.py
@@ -0,0 +1,387 @@
+# Copyright 2012, 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.
+
+
+"""Dispatch WebSocket request.
+"""
+
+
+import logging
+import os
+import re
+
+from mod_pywebsocket import common
+from mod_pywebsocket import handshake
+from mod_pywebsocket import msgutil
+from mod_pywebsocket import mux
+from mod_pywebsocket import stream
+from mod_pywebsocket import util
+
+
+_SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$')
+_SOURCE_SUFFIX = '_wsh.py'
+_DO_EXTRA_HANDSHAKE_HANDLER_NAME = 'web_socket_do_extra_handshake'
+_TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data'
+_PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME = (
+    'web_socket_passive_closing_handshake')
+
+
+class DispatchException(Exception):
+    """Exception in dispatching WebSocket request."""
+
+    def __init__(self, name, status=common.HTTP_STATUS_NOT_FOUND):
+        super(DispatchException, self).__init__(name)
+        self.status = status
+
+
+def _default_passive_closing_handshake_handler(request):
+    """Default web_socket_passive_closing_handshake handler."""
+
+    return common.STATUS_NORMAL_CLOSURE, ''
+
+
+def _normalize_path(path):
+    """Normalize path.
+
+    Args:
+        path: the path to normalize.
+
+    Path is converted to the absolute path.
+    The input path can use either '\\' or '/' as the separator.
+    The normalized path always uses '/' regardless of the platform.
+    """
+
+    path = path.replace('\\', os.path.sep)
+    path = os.path.realpath(path)
+    path = path.replace('\\', '/')
+    return path
+
+
+def _create_path_to_resource_converter(base_dir):
+    """Returns a function that converts the path of a WebSocket handler source
+    file to a resource string by removing the path to the base directory from
+    its head, removing _SOURCE_SUFFIX from its tail, and replacing path
+    separators in it with '/'.
+
+    Args:
+        base_dir: the path to the base directory.
+    """
+
+    base_dir = _normalize_path(base_dir)
+
+    base_len = len(base_dir)
+    suffix_len = len(_SOURCE_SUFFIX)
+
+    def converter(path):
+        if not path.endswith(_SOURCE_SUFFIX):
+            return None
+        # _normalize_path must not be used because resolving symlink breaks
+        # following path check.
+        path = path.replace('\\', '/')
+        if not path.startswith(base_dir):
+            return None
+        return path[base_len:-suffix_len]
+
+    return converter
+
+
+def _enumerate_handler_file_paths(directory):
+    """Returns a generator that enumerates WebSocket Handler source file names
+    in the given directory.
+    """
+
+    for root, unused_dirs, files in os.walk(directory):
+        for base in files:
+            path = os.path.join(root, base)
+            if _SOURCE_PATH_PATTERN.search(path):
+                yield path
+
+
+class _HandlerSuite(object):
+    """A handler suite holder class."""
+
+    def __init__(self, do_extra_handshake, transfer_data,
+                 passive_closing_handshake):
+        self.do_extra_handshake = do_extra_handshake
+        self.transfer_data = transfer_data
+        self.passive_closing_handshake = passive_closing_handshake
+
+
+def _source_handler_file(handler_definition):
+    """Source a handler definition string.
+
+    Args:
+        handler_definition: a string containing Python statements that define
+                            handler functions.
+    """
+
+    global_dic = {}
+    try:
+        exec handler_definition in global_dic
+    except Exception:
+        raise DispatchException('Error in sourcing handler:' +
+                                util.get_stack_trace())
+    passive_closing_handshake_handler = None
+    try:
+        passive_closing_handshake_handler = _extract_handler(
+            global_dic, _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME)
+    except Exception:
+        passive_closing_handshake_handler = (
+            _default_passive_closing_handshake_handler)
+    return _HandlerSuite(
+        _extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME),
+        _extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME),
+        passive_closing_handshake_handler)
+
+
+def _extract_handler(dic, name):
+    """Extracts a callable with the specified name from the given dictionary
+    dic.
+    """
+
+    if name not in dic:
+        raise DispatchException('%s is not defined.' % name)
+    handler = dic[name]
+    if not callable(handler):
+        raise DispatchException('%s is not callable.' % name)
+    return handler
+
+
+class Dispatcher(object):
+    """Dispatches WebSocket requests.
+
+    This class maintains a map from resource name to handlers.
+    """
+
+    def __init__(
+        self, root_dir, scan_dir=None,
+        allow_handlers_outside_root_dir=True):
+        """Construct an instance.
+
+        Args:
+            root_dir: The directory where handler definition files are
+                      placed.
+            scan_dir: The directory where handler definition files are
+                      searched. scan_dir must be a directory under root_dir,
+                      including root_dir itself.  If scan_dir is None,
+                      root_dir is used as scan_dir. scan_dir can be useful
+                      in saving scan time when root_dir contains many
+                      subdirectories.
+            allow_handlers_outside_root_dir: Scans handler files even if their
+                      canonical path is not under root_dir.
+        """
+
+        self._logger = util.get_class_logger(self)
+
+        self._handler_suite_map = {}
+        self._source_warnings = []
+        if scan_dir is None:
+            scan_dir = root_dir
+        if not os.path.realpath(scan_dir).startswith(
+                os.path.realpath(root_dir)):
+            raise DispatchException('scan_dir:%s must be a directory under '
+                                    'root_dir:%s.' % (scan_dir, root_dir))
+        self._source_handler_files_in_dir(
+            root_dir, scan_dir, allow_handlers_outside_root_dir)
+
+    def add_resource_path_alias(self,
+                                alias_resource_path, existing_resource_path):
+        """Add resource path alias.
+
+        Once added, request to alias_resource_path would be handled by
+        handler registered for existing_resource_path.
+
+        Args:
+            alias_resource_path: alias resource path
+            existing_resource_path: existing resource path
+        """
+        try:
+            handler_suite = self._handler_suite_map[existing_resource_path]
+            self._handler_suite_map[alias_resource_path] = handler_suite
+        except KeyError:
+            raise DispatchException('No handler for: %r' %
+                                    existing_resource_path)
+
+    def source_warnings(self):
+        """Return warnings in sourcing handlers."""
+
+        return self._source_warnings
+
+    def do_extra_handshake(self, request):
+        """Do extra checking in WebSocket handshake.
+
+        Select a handler based on request.uri and call its
+        web_socket_do_extra_handshake function.
+
+        Args:
+            request: mod_python request.
+
+        Raises:
+            DispatchException: when handler was not found
+            AbortedByUserException: when user handler abort connection
+            HandshakeException: when opening handshake failed
+        """
+
+        handler_suite = self.get_handler_suite(request.ws_resource)
+        if handler_suite is None:
+            raise DispatchException('No handler for: %r' % request.ws_resource)
+        do_extra_handshake_ = handler_suite.do_extra_handshake
+        try:
+            do_extra_handshake_(request)
+        except handshake.AbortedByUserException, e:
+            raise
+        except Exception, e:
+            util.prepend_message_to_exception(
+                    '%s raised exception for %s: ' % (
+                            _DO_EXTRA_HANDSHAKE_HANDLER_NAME,
+                            request.ws_resource),
+                    e)
+            raise handshake.HandshakeException(e, common.HTTP_STATUS_FORBIDDEN)
+
+    def transfer_data(self, request):
+        """Let a handler transfer_data with a WebSocket client.
+
+        Select a handler based on request.ws_resource and call its
+        web_socket_transfer_data function.
+
+        Args:
+            request: mod_python request.
+
+        Raises:
+            DispatchException: when handler was not found
+            AbortedByUserException: when user handler abort connection
+        """
+
+        # TODO(tyoshino): Terminate underlying TCP connection if possible.
+        try:
+            if mux.use_mux(request):
+                mux.start(request, self)
+            else:
+                handler_suite = self.get_handler_suite(request.ws_resource)
+                if handler_suite is None:
+                    raise DispatchException('No handler for: %r' %
+                                            request.ws_resource)
+                transfer_data_ = handler_suite.transfer_data
+                transfer_data_(request)
+
+            if not request.server_terminated:
+                request.ws_stream.close_connection()
+        # Catch non-critical exceptions the handler didn't handle.
+        except handshake.AbortedByUserException, e:
+            self._logger.debug('%s', e)
+            raise
+        except msgutil.BadOperationException, e:
+            self._logger.debug('%s', e)
+            request.ws_stream.close_connection(common.STATUS_ABNORMAL_CLOSURE)
+        except msgutil.InvalidFrameException, e:
+            # InvalidFrameException must be caught before
+            # ConnectionTerminatedException that catches InvalidFrameException.
+            self._logger.debug('%s', e)
+            request.ws_stream.close_connection(common.STATUS_PROTOCOL_ERROR)
+        except msgutil.UnsupportedFrameException, e:
+            self._logger.debug('%s', e)
+            request.ws_stream.close_connection(common.STATUS_UNSUPPORTED_DATA)
+        except stream.InvalidUTF8Exception, e:
+            self._logger.debug('%s', e)
+            request.ws_stream.close_connection(
+                common.STATUS_INVALID_FRAME_PAYLOAD_DATA)
+        except msgutil.ConnectionTerminatedException, e:
+            self._logger.debug('%s', e)
+        except Exception, e:
+            util.prepend_message_to_exception(
+                '%s raised exception for %s: ' % (
+                    _TRANSFER_DATA_HANDLER_NAME, request.ws_resource),
+                e)
+            raise
+
+    def passive_closing_handshake(self, request):
+        """Prepare code and reason for responding client initiated closing
+        handshake.
+        """
+
+        handler_suite = self.get_handler_suite(request.ws_resource)
+        if handler_suite is None:
+            return _default_passive_closing_handshake_handler(request)
+        return handler_suite.passive_closing_handshake(request)
+
+    def get_handler_suite(self, resource):
+        """Retrieves two handlers (one for extra handshake processing, and one
+        for data transfer) for the given request as a HandlerSuite object.
+        """
+
+        fragment = None
+        if '#' in resource:
+            resource, fragment = resource.split('#', 1)
+        if '?' in resource:
+            resource = resource.split('?', 1)[0]
+        handler_suite = self._handler_suite_map.get(resource)
+        if handler_suite and fragment:
+            raise DispatchException('Fragment identifiers MUST NOT be used on '
+                                    'WebSocket URIs',
+                                    common.HTTP_STATUS_BAD_REQUEST)
+        return handler_suite
+
+    def _source_handler_files_in_dir(
+        self, root_dir, scan_dir, allow_handlers_outside_root_dir):
+        """Source all the handler source files in the scan_dir directory.
+
+        The resource path is determined relative to root_dir.
+        """
+
+        # We build a map from resource to handler code assuming that there's
+        # only one path from root_dir to scan_dir and it can be obtained by
+        # comparing realpath of them.
+
+        # Here we cannot use abspath. See
+        # https://bugs.webkit.org/show_bug.cgi?id=31603
+
+        convert = _create_path_to_resource_converter(root_dir)
+        scan_realpath = os.path.realpath(scan_dir)
+        root_realpath = os.path.realpath(root_dir)
+        for path in _enumerate_handler_file_paths(scan_realpath):
+            if (not allow_handlers_outside_root_dir and
+                (not os.path.realpath(path).startswith(root_realpath))):
+                self._logger.debug(
+                    'Canonical path of %s is not under root directory' %
+                    path)
+                continue
+            try:
+                handler_suite = _source_handler_file(open(path).read())
+            except DispatchException, e:
+                self._source_warnings.append('%s: %s' % (path, e))
+                continue
+            resource = convert(path)
+            if resource is None:
+                self._logger.debug(
+                    'Path to resource conversion on %s failed' % path)
+            else:
+                self._handler_suite_map[convert(path)] = handler_suite
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/extensions.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/extensions.py
new file mode 100644
index 0000000..03dbf9e
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/extensions.py
@@ -0,0 +1,727 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+from mod_pywebsocket import common
+from mod_pywebsocket import util
+from mod_pywebsocket.http_header_util import quote_if_necessary
+
+
+_available_processors = {}
+
+
+class ExtensionProcessorInterface(object):
+
+    def name(self):
+        return None
+
+    def get_extension_response(self):
+        return None
+
+    def setup_stream_options(self, stream_options):
+        pass
+
+
+class DeflateStreamExtensionProcessor(ExtensionProcessorInterface):
+    """WebSocket DEFLATE stream extension processor.
+
+    Specification:
+    Section 9.2.1 in
+    http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
+    """
+
+    def __init__(self, request):
+        self._logger = util.get_class_logger(self)
+
+        self._request = request
+
+    def name(self):
+        return common.DEFLATE_STREAM_EXTENSION
+
+    def get_extension_response(self):
+        if len(self._request.get_parameter_names()) != 0:
+            return None
+
+        self._logger.debug(
+            'Enable %s extension', common.DEFLATE_STREAM_EXTENSION)
+
+        return common.ExtensionParameter(common.DEFLATE_STREAM_EXTENSION)
+
+    def setup_stream_options(self, stream_options):
+        stream_options.deflate_stream = True
+
+
+_available_processors[common.DEFLATE_STREAM_EXTENSION] = (
+    DeflateStreamExtensionProcessor)
+
+
+def _log_compression_ratio(logger, original_bytes, total_original_bytes,
+                           filtered_bytes, total_filtered_bytes):
+    # Print inf when ratio is not available.
+    ratio = float('inf')
+    average_ratio = float('inf')
+    if original_bytes != 0:
+        ratio = float(filtered_bytes) / original_bytes
+    if total_original_bytes != 0:
+        average_ratio = (
+            float(total_filtered_bytes) / total_original_bytes)
+    logger.debug('Outgoing compress ratio: %f (average: %f)' %
+        (ratio, average_ratio))
+
+
+def _log_decompression_ratio(logger, received_bytes, total_received_bytes,
+                             filtered_bytes, total_filtered_bytes):
+    # Print inf when ratio is not available.
+    ratio = float('inf')
+    average_ratio = float('inf')
+    if received_bytes != 0:
+        ratio = float(received_bytes) / filtered_bytes
+    if total_filtered_bytes != 0:
+        average_ratio = (
+            float(total_received_bytes) / total_filtered_bytes)
+    logger.debug('Incoming compress ratio: %f (average: %f)' %
+        (ratio, average_ratio))
+
+
+class DeflateFrameExtensionProcessor(ExtensionProcessorInterface):
+    """WebSocket Per-frame DEFLATE extension processor.
+
+    Specification:
+    http://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate
+    """
+
+    _WINDOW_BITS_PARAM = 'max_window_bits'
+    _NO_CONTEXT_TAKEOVER_PARAM = 'no_context_takeover'
+
+    def __init__(self, request):
+        self._logger = util.get_class_logger(self)
+
+        self._request = request
+
+        self._response_window_bits = None
+        self._response_no_context_takeover = False
+        self._bfinal = False
+
+        # Counters for statistics.
+
+        # Total number of outgoing bytes supplied to this filter.
+        self._total_outgoing_payload_bytes = 0
+        # Total number of bytes sent to the network after applying this filter.
+        self._total_filtered_outgoing_payload_bytes = 0
+
+        # Total number of bytes received from the network.
+        self._total_incoming_payload_bytes = 0
+        # Total number of incoming bytes obtained after applying this filter.
+        self._total_filtered_incoming_payload_bytes = 0
+
+    def name(self):
+        return common.DEFLATE_FRAME_EXTENSION
+
+    def get_extension_response(self):
+        # Any unknown parameter will be just ignored.
+
+        window_bits = self._request.get_parameter_value(
+            self._WINDOW_BITS_PARAM)
+        no_context_takeover = self._request.has_parameter(
+            self._NO_CONTEXT_TAKEOVER_PARAM)
+        if (no_context_takeover and
+            self._request.get_parameter_value(
+                self._NO_CONTEXT_TAKEOVER_PARAM) is not None):
+            return None
+
+        if window_bits is not None:
+            try:
+                window_bits = int(window_bits)
+            except ValueError, e:
+                return None
+            if window_bits < 8 or window_bits > 15:
+                return None
+
+        self._deflater = util._RFC1979Deflater(
+            window_bits, no_context_takeover)
+
+        self._inflater = util._RFC1979Inflater()
+
+        self._compress_outgoing = True
+
+        response = common.ExtensionParameter(self._request.name())
+
+        if self._response_window_bits is not None:
+            response.add_parameter(
+                self._WINDOW_BITS_PARAM, str(self._response_window_bits))
+        if self._response_no_context_takeover:
+            response.add_parameter(
+                self._NO_CONTEXT_TAKEOVER_PARAM, None)
+
+        self._logger.debug(
+            'Enable %s extension ('
+            'request: window_bits=%s; no_context_takeover=%r, '
+            'response: window_wbits=%s; no_context_takeover=%r)' %
+            (self._request.name(),
+             window_bits,
+             no_context_takeover,
+             self._response_window_bits,
+             self._response_no_context_takeover))
+
+        return response
+
+    def setup_stream_options(self, stream_options):
+
+        class _OutgoingFilter(object):
+
+            def __init__(self, parent):
+                self._parent = parent
+
+            def filter(self, frame):
+                self._parent._outgoing_filter(frame)
+
+        class _IncomingFilter(object):
+
+            def __init__(self, parent):
+                self._parent = parent
+
+            def filter(self, frame):
+                self._parent._incoming_filter(frame)
+
+        stream_options.outgoing_frame_filters.append(
+            _OutgoingFilter(self))
+        stream_options.incoming_frame_filters.insert(
+            0, _IncomingFilter(self))
+
+    def set_response_window_bits(self, value):
+        self._response_window_bits = value
+
+    def set_response_no_context_takeover(self, value):
+        self._response_no_context_takeover = value
+
+    def set_bfinal(self, value):
+        self._bfinal = value
+
+    def enable_outgoing_compression(self):
+        self._compress_outgoing = True
+
+    def disable_outgoing_compression(self):
+        self._compress_outgoing = False
+
+    def _outgoing_filter(self, frame):
+        """Transform outgoing frames. This method is called only by
+        an _OutgoingFilter instance.
+        """
+
+        original_payload_size = len(frame.payload)
+        self._total_outgoing_payload_bytes += original_payload_size
+
+        if (not self._compress_outgoing or
+            common.is_control_opcode(frame.opcode)):
+            self._total_filtered_outgoing_payload_bytes += (
+                original_payload_size)
+            return
+
+        frame.payload = self._deflater.filter(
+            frame.payload, bfinal=self._bfinal)
+        frame.rsv1 = 1
+
+        filtered_payload_size = len(frame.payload)
+        self._total_filtered_outgoing_payload_bytes += filtered_payload_size
+
+        _log_compression_ratio(self._logger, original_payload_size,
+                               self._total_outgoing_payload_bytes,
+                               filtered_payload_size,
+                               self._total_filtered_outgoing_payload_bytes)
+
+    def _incoming_filter(self, frame):
+        """Transform incoming frames. This method is called only by
+        an _IncomingFilter instance.
+        """
+
+        received_payload_size = len(frame.payload)
+        self._total_incoming_payload_bytes += received_payload_size
+
+        if frame.rsv1 != 1 or common.is_control_opcode(frame.opcode):
+            self._total_filtered_incoming_payload_bytes += (
+                received_payload_size)
+            return
+
+        frame.payload = self._inflater.filter(frame.payload)
+        frame.rsv1 = 0
+
+        filtered_payload_size = len(frame.payload)
+        self._total_filtered_incoming_payload_bytes += filtered_payload_size
+
+        _log_decompression_ratio(self._logger, received_payload_size,
+                                 self._total_incoming_payload_bytes,
+                                 filtered_payload_size,
+                                 self._total_filtered_incoming_payload_bytes)
+
+
+_available_processors[common.DEFLATE_FRAME_EXTENSION] = (
+    DeflateFrameExtensionProcessor)
+
+
+# Adding vendor-prefixed deflate-frame extension.
+# TODO(bashi): Remove this after WebKit stops using vendor prefix.
+_available_processors[common.X_WEBKIT_DEFLATE_FRAME_EXTENSION] = (
+    DeflateFrameExtensionProcessor)
+
+
+def _parse_compression_method(data):
+    """Parses the value of "method" extension parameter."""
+
+    return common.parse_extensions(data, allow_quoted_string=True)
+
+
+def _create_accepted_method_desc(method_name, method_params):
+    """Creates accepted-method-desc from given method name and parameters"""
+
+    extension = common.ExtensionParameter(method_name)
+    for name, value in method_params:
+        extension.add_parameter(name, value)
+    return common.format_extension(extension)
+
+
+class CompressionExtensionProcessorBase(ExtensionProcessorInterface):
+    """Base class for Per-frame and Per-message compression extension."""
+
+    _METHOD_PARAM = 'method'
+
+    def __init__(self, request):
+        self._logger = util.get_class_logger(self)
+        self._request = request
+        self._compression_method_name = None
+        self._compression_processor = None
+        self._compression_processor_hook = None
+
+    def name(self):
+        return ''
+
+    def _lookup_compression_processor(self, method_desc):
+        return None
+
+    def _get_compression_processor_response(self):
+        """Looks up the compression processor based on the self._request and
+           returns the compression processor's response.
+        """
+
+        method_list = self._request.get_parameter_value(self._METHOD_PARAM)
+        if method_list is None:
+            return None
+        methods = _parse_compression_method(method_list)
+        if methods is None:
+            return None
+        comression_processor = None
+        # The current implementation tries only the first method that matches
+        # supported algorithm. Following methods aren't tried even if the
+        # first one is rejected.
+        # TODO(bashi): Need to clarify this behavior.
+        for method_desc in methods:
+            compression_processor = self._lookup_compression_processor(
+                method_desc)
+            if compression_processor is not None:
+                self._compression_method_name = method_desc.name()
+                break
+        if compression_processor is None:
+            return None
+
+        if self._compression_processor_hook:
+            self._compression_processor_hook(compression_processor)
+
+        processor_response = compression_processor.get_extension_response()
+        if processor_response is None:
+            return None
+        self._compression_processor = compression_processor
+        return processor_response
+
+    def get_extension_response(self):
+        processor_response = self._get_compression_processor_response()
+        if processor_response is None:
+            return None
+
+        response = common.ExtensionParameter(self._request.name())
+        accepted_method_desc = _create_accepted_method_desc(
+                                   self._compression_method_name,
+                                   processor_response.get_parameters())
+        response.add_parameter(self._METHOD_PARAM, accepted_method_desc)
+        self._logger.debug(
+            'Enable %s extension (method: %s)' %
+            (self._request.name(), self._compression_method_name))
+        return response
+
+    def setup_stream_options(self, stream_options):
+        if self._compression_processor is None:
+            return
+        self._compression_processor.setup_stream_options(stream_options)
+
+    def set_compression_processor_hook(self, hook):
+        self._compression_processor_hook = hook
+
+    def get_compression_processor(self):
+        return self._compression_processor
+
+
+class PerFrameCompressionExtensionProcessor(CompressionExtensionProcessorBase):
+    """WebSocket Per-frame compression extension processor.
+
+    Specification:
+    http://tools.ietf.org/html/draft-ietf-hybi-websocket-perframe-compression
+    """
+
+    _DEFLATE_METHOD = 'deflate'
+
+    def __init__(self, request):
+        CompressionExtensionProcessorBase.__init__(self, request)
+
+    def name(self):
+        return common.PERFRAME_COMPRESSION_EXTENSION
+
+    def _lookup_compression_processor(self, method_desc):
+        if method_desc.name() == self._DEFLATE_METHOD:
+            return DeflateFrameExtensionProcessor(method_desc)
+        return None
+
+
+_available_processors[common.PERFRAME_COMPRESSION_EXTENSION] = (
+    PerFrameCompressionExtensionProcessor)
+
+
+class DeflateMessageProcessor(ExtensionProcessorInterface):
+    """Per-message deflate processor."""
+
+    _S2C_MAX_WINDOW_BITS_PARAM = 's2c_max_window_bits'
+    _S2C_NO_CONTEXT_TAKEOVER_PARAM = 's2c_no_context_takeover'
+    _C2S_MAX_WINDOW_BITS_PARAM = 'c2s_max_window_bits'
+    _C2S_NO_CONTEXT_TAKEOVER_PARAM = 'c2s_no_context_takeover'
+
+    def __init__(self, request):
+        self._request = request
+        self._logger = util.get_class_logger(self)
+
+        self._c2s_max_window_bits = None
+        self._c2s_no_context_takeover = False
+        self._bfinal = False
+
+        self._compress_outgoing_enabled = False
+
+        # True if a message is fragmented and compression is ongoing.
+        self._compress_ongoing = False
+
+        # Counters for statistics.
+
+        # Total number of outgoing bytes supplied to this filter.
+        self._total_outgoing_payload_bytes = 0
+        # Total number of bytes sent to the network after applying this filter.
+        self._total_filtered_outgoing_payload_bytes = 0
+
+        # Total number of bytes received from the network.
+        self._total_incoming_payload_bytes = 0
+        # Total number of incoming bytes obtained after applying this filter.
+        self._total_filtered_incoming_payload_bytes = 0
+
+    def name(self):
+        return 'deflate'
+
+    def get_extension_response(self):
+        # Any unknown parameter will be just ignored.
+
+        s2c_max_window_bits = self._request.get_parameter_value(
+            self._S2C_MAX_WINDOW_BITS_PARAM)
+        if s2c_max_window_bits is not None:
+            try:
+                s2c_max_window_bits = int(s2c_max_window_bits)
+            except ValueError, e:
+                return None
+            if s2c_max_window_bits < 8 or s2c_max_window_bits > 15:
+                return None
+
+        s2c_no_context_takeover = self._request.has_parameter(
+            self._S2C_NO_CONTEXT_TAKEOVER_PARAM)
+        if (s2c_no_context_takeover and
+            self._request.get_parameter_value(
+                self._S2C_NO_CONTEXT_TAKEOVER_PARAM) is not None):
+            return None
+
+        self._deflater = util._RFC1979Deflater(
+            s2c_max_window_bits, s2c_no_context_takeover)
+
+        self._inflater = util._RFC1979Inflater()
+
+        self._compress_outgoing_enabled = True
+
+        response = common.ExtensionParameter(self._request.name())
+
+        if s2c_max_window_bits is not None:
+            response.add_parameter(
+                self._S2C_MAX_WINDOW_BITS_PARAM, str(s2c_max_window_bits))
+
+        if s2c_no_context_takeover:
+            response.add_parameter(
+                self._S2C_NO_CONTEXT_TAKEOVER_PARAM, None)
+
+        if self._c2s_max_window_bits is not None:
+            response.add_parameter(
+                self._C2S_MAX_WINDOW_BITS_PARAM,
+                str(self._c2s_max_window_bits))
+        if self._c2s_no_context_takeover:
+            response.add_parameter(
+                self._C2S_NO_CONTEXT_TAKEOVER_PARAM, None)
+
+        self._logger.debug(
+            'Enable %s extension ('
+            'request: s2c_max_window_bits=%s; s2c_no_context_takeover=%r, '
+            'response: c2s_max_window_bits=%s; c2s_no_context_takeover=%r)' %
+            (self._request.name(),
+             s2c_max_window_bits,
+             s2c_no_context_takeover,
+             self._c2s_max_window_bits,
+             self._c2s_no_context_takeover))
+
+        return response
+
+    def setup_stream_options(self, stream_options):
+        class _OutgoingMessageFilter(object):
+
+            def __init__(self, parent):
+                self._parent = parent
+
+            def filter(self, message, end=True, binary=False):
+                return self._parent._process_outgoing_message(
+                    message, end, binary)
+
+        class _IncomingMessageFilter(object):
+
+            def __init__(self, parent):
+                self._parent = parent
+                self._decompress_next_message = False
+
+            def decompress_next_message(self):
+                self._decompress_next_message = True
+
+            def filter(self, message):
+                message = self._parent._process_incoming_message(
+                    message, self._decompress_next_message)
+                self._decompress_next_message = False
+                return message
+
+        self._outgoing_message_filter = _OutgoingMessageFilter(self)
+        self._incoming_message_filter = _IncomingMessageFilter(self)
+        stream_options.outgoing_message_filters.append(
+            self._outgoing_message_filter)
+        stream_options.incoming_message_filters.append(
+            self._incoming_message_filter)
+
+        class _OutgoingFrameFilter(object):
+
+            def __init__(self, parent):
+                self._parent = parent
+                self._set_compression_bit = False
+
+            def set_compression_bit(self):
+                self._set_compression_bit = True
+
+            def filter(self, frame):
+                self._parent._process_outgoing_frame(
+                    frame, self._set_compression_bit)
+                self._set_compression_bit = False
+
+        class _IncomingFrameFilter(object):
+
+            def __init__(self, parent):
+                self._parent = parent
+
+            def filter(self, frame):
+                self._parent._process_incoming_frame(frame)
+
+        self._outgoing_frame_filter = _OutgoingFrameFilter(self)
+        self._incoming_frame_filter = _IncomingFrameFilter(self)
+        stream_options.outgoing_frame_filters.append(
+            self._outgoing_frame_filter)
+        stream_options.incoming_frame_filters.append(
+            self._incoming_frame_filter)
+
+        stream_options.encode_text_message_to_utf8 = False
+
+    def set_c2s_max_window_bits(self, value):
+        self._c2s_max_window_bits = value
+
+    def set_c2s_no_context_takeover(self, value):
+        self._c2s_no_context_takeover = value
+
+    def set_bfinal(self, value):
+        self._bfinal = value
+
+    def enable_outgoing_compression(self):
+        self._compress_outgoing_enabled = True
+
+    def disable_outgoing_compression(self):
+        self._compress_outgoing_enabled = False
+
+    def _process_incoming_message(self, message, decompress):
+        if not decompress:
+            return message
+
+        received_payload_size = len(message)
+        self._total_incoming_payload_bytes += received_payload_size
+
+        message = self._inflater.filter(message)
+
+        filtered_payload_size = len(message)
+        self._total_filtered_incoming_payload_bytes += filtered_payload_size
+
+        _log_decompression_ratio(self._logger, received_payload_size,
+                                 self._total_incoming_payload_bytes,
+                                 filtered_payload_size,
+                                 self._total_filtered_incoming_payload_bytes)
+
+        return message
+
+    def _process_outgoing_message(self, message, end, binary):
+        if not binary:
+            message = message.encode('utf-8')
+
+        if not self._compress_outgoing_enabled:
+            return message
+
+        original_payload_size = len(message)
+        self._total_outgoing_payload_bytes += original_payload_size
+
+        message = self._deflater.filter(
+            message, flush=end, bfinal=self._bfinal)
+
+        filtered_payload_size = len(message)
+        self._total_filtered_outgoing_payload_bytes += filtered_payload_size
+
+        _log_compression_ratio(self._logger, original_payload_size,
+                               self._total_outgoing_payload_bytes,
+                               filtered_payload_size,
+                               self._total_filtered_outgoing_payload_bytes)
+
+        if not self._compress_ongoing:
+            self._outgoing_frame_filter.set_compression_bit()
+        self._compress_ongoing = not end
+        return message
+
+    def _process_incoming_frame(self, frame):
+        if frame.rsv1 == 1 and not common.is_control_opcode(frame.opcode):
+            self._incoming_message_filter.decompress_next_message()
+            frame.rsv1 = 0
+
+    def _process_outgoing_frame(self, frame, compression_bit):
+        if (not compression_bit or
+            common.is_control_opcode(frame.opcode)):
+            return
+
+        frame.rsv1 = 1
+
+
+class PerMessageCompressionExtensionProcessor(
+    CompressionExtensionProcessorBase):
+    """WebSocket Per-message compression extension processor.
+
+    Specification:
+    http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression
+    """
+
+    _DEFLATE_METHOD = 'deflate'
+
+    def __init__(self, request):
+        CompressionExtensionProcessorBase.__init__(self, request)
+
+    def name(self):
+        return common.PERMESSAGE_COMPRESSION_EXTENSION
+
+    def _lookup_compression_processor(self, method_desc):
+        if method_desc.name() == self._DEFLATE_METHOD:
+            return DeflateMessageProcessor(method_desc)
+        return None
+
+
+_available_processors[common.PERMESSAGE_COMPRESSION_EXTENSION] = (
+    PerMessageCompressionExtensionProcessor)
+
+
+# Adding vendor-prefixed permessage-compress extension.
+# TODO(bashi): Remove this after WebKit stops using vendor prefix.
+_available_processors[common.X_WEBKIT_PERMESSAGE_COMPRESSION_EXTENSION] = (
+    PerMessageCompressionExtensionProcessor)
+
+
+class MuxExtensionProcessor(ExtensionProcessorInterface):
+    """WebSocket multiplexing extension processor."""
+
+    _QUOTA_PARAM = 'quota'
+
+    def __init__(self, request):
+        self._request = request
+
+    def name(self):
+        return common.MUX_EXTENSION
+
+    def get_extension_response(self, ws_request,
+                               logical_channel_extensions):
+        # Mux extension cannot be used after extensions that depend on
+        # frame boundary, extension data field, or any reserved bits
+        # which are attributed to each frame.
+        for extension in logical_channel_extensions:
+            name = extension.name()
+            if (name == common.PERFRAME_COMPRESSION_EXTENSION or
+                name == common.DEFLATE_FRAME_EXTENSION or
+                name == common.X_WEBKIT_DEFLATE_FRAME_EXTENSION):
+                return None
+
+        quota = self._request.get_parameter_value(self._QUOTA_PARAM)
+        if quota is None:
+            ws_request.mux_quota = 0
+        else:
+            try:
+                quota = int(quota)
+            except ValueError, e:
+                return None
+            if quota < 0 or quota >= 2 ** 32:
+                return None
+            ws_request.mux_quota = quota
+
+        ws_request.mux = True
+        ws_request.mux_extensions = logical_channel_extensions
+        return common.ExtensionParameter(common.MUX_EXTENSION)
+
+    def setup_stream_options(self, stream_options):
+        pass
+
+
+_available_processors[common.MUX_EXTENSION] = MuxExtensionProcessor
+
+
+def get_extension_processor(extension_request):
+    global _available_processors
+    processor_class = _available_processors.get(extension_request.name())
+    if processor_class is None:
+        return None
+    return processor_class(extension_request)
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/__init__.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/__init__.py
new file mode 100644
index 0000000..194f6b3
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/__init__.py
@@ -0,0 +1,110 @@
+# Copyright 2011, 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.
+
+
+"""WebSocket opening handshake processor. This class try to apply available
+opening handshake processors for each protocol version until a connection is
+successfully established.
+"""
+
+
+import logging
+
+from mod_pywebsocket import common
+from mod_pywebsocket.handshake import hybi00
+from mod_pywebsocket.handshake import hybi
+# Export AbortedByUserException, HandshakeException, and VersionException
+# symbol from this module.
+from mod_pywebsocket.handshake._base import AbortedByUserException
+from mod_pywebsocket.handshake._base import HandshakeException
+from mod_pywebsocket.handshake._base import VersionException
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def do_handshake(request, dispatcher, allowDraft75=False, strict=False):
+    """Performs WebSocket handshake.
+
+    Args:
+        request: mod_python request.
+        dispatcher: Dispatcher (dispatch.Dispatcher).
+        allowDraft75: obsolete argument. ignored.
+        strict: obsolete argument. ignored.
+
+    Handshaker will add attributes such as ws_resource in performing
+    handshake.
+    """
+
+    _LOGGER.debug('Client\'s opening handshake resource: %r', request.uri)
+    # To print mimetools.Message as escaped one-line string, we converts
+    # headers_in to dict object. Without conversion, if we use %r, it just
+    # prints the type and address, and if we use %s, it prints the original
+    # header string as multiple lines.
+    #
+    # Both mimetools.Message and MpTable_Type of mod_python can be
+    # converted to dict.
+    #
+    # mimetools.Message.__str__ returns the original header string.
+    # dict(mimetools.Message object) returns the map from header names to
+    # header values. While MpTable_Type doesn't have such __str__ but just
+    # __repr__ which formats itself as well as dictionary object.
+    _LOGGER.debug(
+        'Client\'s opening handshake headers: %r', dict(request.headers_in))
+
+    handshakers = []
+    handshakers.append(
+        ('RFC 6455', hybi.Handshaker(request, dispatcher)))
+    handshakers.append(
+        ('HyBi 00', hybi00.Handshaker(request, dispatcher)))
+
+    for name, handshaker in handshakers:
+        _LOGGER.debug('Trying protocol version %s', name)
+        try:
+            handshaker.do_handshake()
+            _LOGGER.info('Established (%s protocol)', name)
+            return
+        except HandshakeException, e:
+            _LOGGER.debug(
+                'Failed to complete opening handshake as %s protocol: %r',
+                name, e)
+            if e.status:
+                raise e
+        except AbortedByUserException, e:
+            raise
+        except VersionException, e:
+            raise
+
+    # TODO(toyoshim): Add a test to cover the case all handshakers fail.
+    raise HandshakeException(
+        'Failed to complete opening handshake for all available protocols',
+        status=common.HTTP_STATUS_BAD_REQUEST)
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/_base.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/_base.py
new file mode 100644
index 0000000..e5c94ca
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/_base.py
@@ -0,0 +1,226 @@
+# Copyright 2012, 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.
+
+
+"""Common functions and exceptions used by WebSocket opening handshake
+processors.
+"""
+
+
+from mod_pywebsocket import common
+from mod_pywebsocket import http_header_util
+
+
+class AbortedByUserException(Exception):
+    """Exception for aborting a connection intentionally.
+
+    If this exception is raised in do_extra_handshake handler, the connection
+    will be abandoned. No other WebSocket or HTTP(S) handler will be invoked.
+
+    If this exception is raised in transfer_data_handler, the connection will
+    be closed without closing handshake. No other WebSocket or HTTP(S) handler
+    will be invoked.
+    """
+
+    pass
+
+
+class HandshakeException(Exception):
+    """This exception will be raised when an error occurred while processing
+    WebSocket initial handshake.
+    """
+
+    def __init__(self, name, status=None):
+        super(HandshakeException, self).__init__(name)
+        self.status = status
+
+
+class VersionException(Exception):
+    """This exception will be raised when a version of client request does not
+    match with version the server supports.
+    """
+
+    def __init__(self, name, supported_versions=''):
+        """Construct an instance.
+
+        Args:
+            supported_version: a str object to show supported hybi versions.
+                               (e.g. '8, 13')
+        """
+        super(VersionException, self).__init__(name)
+        self.supported_versions = supported_versions
+
+
+def get_default_port(is_secure):
+    if is_secure:
+        return common.DEFAULT_WEB_SOCKET_SECURE_PORT
+    else:
+        return common.DEFAULT_WEB_SOCKET_PORT
+
+
+def validate_subprotocol(subprotocol, hixie):
+    """Validate a value in the Sec-WebSocket-Protocol field.
+
+    See
+    - RFC 6455: Section 4.1., 4.2.2., and 4.3.
+    - HyBi 00: Section 4.1. Opening handshake
+
+    Args:
+         hixie: if True, checks if characters in subprotocol are in range
+                between U+0020 and U+007E. It's required by HyBi 00 but not by
+                RFC 6455.
+    """
+
+    if not subprotocol:
+        raise HandshakeException('Invalid subprotocol name: empty')
+    if hixie:
+        # Parameter should be in the range U+0020 to U+007E.
+        for c in subprotocol:
+            if not 0x20 <= ord(c) <= 0x7e:
+                raise HandshakeException(
+                    'Illegal character in subprotocol name: %r' % c)
+    else:
+        # Parameter should be encoded HTTP token.
+        state = http_header_util.ParsingState(subprotocol)
+        token = http_header_util.consume_token(state)
+        rest = http_header_util.peek(state)
+        # If |rest| is not None, |subprotocol| is not one token or invalid. If
+        # |rest| is None, |token| must not be None because |subprotocol| is
+        # concatenation of |token| and |rest| and is not None.
+        if rest is not None:
+            raise HandshakeException('Invalid non-token string in subprotocol '
+                                     'name: %r' % rest)
+
+
+def parse_host_header(request):
+    fields = request.headers_in['Host'].split(':', 1)
+    if len(fields) == 1:
+        return fields[0], get_default_port(request.is_https())
+    try:
+        return fields[0], int(fields[1])
+    except ValueError, e:
+        raise HandshakeException('Invalid port number format: %r' % e)
+
+
+def format_header(name, value):
+    return '%s: %s\r\n' % (name, value)
+
+
+def build_location(request):
+    """Build WebSocket location for request."""
+    location_parts = []
+    if request.is_https():
+        location_parts.append(common.WEB_SOCKET_SECURE_SCHEME)
+    else:
+        location_parts.append(common.WEB_SOCKET_SCHEME)
+    location_parts.append('://')
+    host, port = parse_host_header(request)
+    connection_port = request.connection.local_addr[1]
+    if port != connection_port:
+        raise HandshakeException('Header/connection port mismatch: %d/%d' %
+                                 (port, connection_port))
+    location_parts.append(host)
+    if (port != get_default_port(request.is_https())):
+        location_parts.append(':')
+        location_parts.append(str(port))
+    location_parts.append(request.uri)
+    return ''.join(location_parts)
+
+
+def get_mandatory_header(request, key):
+    value = request.headers_in.get(key)
+    if value is None:
+        raise HandshakeException('Header %s is not defined' % key)
+    return value
+
+
+def validate_mandatory_header(request, key, expected_value, fail_status=None):
+    value = get_mandatory_header(request, key)
+
+    if value.lower() != expected_value.lower():
+        raise HandshakeException(
+            'Expected %r for header %s but found %r (case-insensitive)' %
+            (expected_value, key, value), status=fail_status)
+
+
+def check_request_line(request):
+    # 5.1 1. The three character UTF-8 string "GET".
+    # 5.1 2. A UTF-8-encoded U+0020 SPACE character (0x20 byte).
+    if request.method != 'GET':
+        raise HandshakeException('Method is not GET: %r' % request.method)
+
+    if request.protocol != 'HTTP/1.1':
+        raise HandshakeException('Version is not HTTP/1.1: %r' %
+                                 request.protocol)
+
+
+def check_header_lines(request, mandatory_headers):
+    check_request_line(request)
+
+    # The expected field names, and the meaning of their corresponding
+    # values, are as follows.
+    #  |Upgrade| and |Connection|
+    for key, expected_value in mandatory_headers:
+        validate_mandatory_header(request, key, expected_value)
+
+
+def parse_token_list(data):
+    """Parses a header value which follows 1#token and returns parsed elements
+    as a list of strings.
+
+    Leading LWSes must be trimmed.
+    """
+
+    state = http_header_util.ParsingState(data)
+
+    token_list = []
+
+    while True:
+        token = http_header_util.consume_token(state)
+        if token is not None:
+            token_list.append(token)
+
+        http_header_util.consume_lwses(state)
+
+        if http_header_util.peek(state) is None:
+            break
+
+        if not http_header_util.consume_string(state, ','):
+            raise HandshakeException(
+                'Expected a comma but found %r' % http_header_util.peek(state))
+
+        http_header_util.consume_lwses(state)
+
+    if len(token_list) == 0:
+        raise HandshakeException('No valid token found')
+
+    return token_list
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/hybi.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/hybi.py
new file mode 100644
index 0000000..fc0e2a0
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/hybi.py
@@ -0,0 +1,404 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""This file provides the opening handshake processor for the WebSocket
+protocol (RFC 6455).
+
+Specification:
+http://tools.ietf.org/html/rfc6455
+"""
+
+
+# Note: request.connection.write is used in this module, even though mod_python
+# document says that it should be used only in connection handlers.
+# Unfortunately, we have no other options. For example, request.write is not
+# suitable because it doesn't allow direct raw bytes writing.
+
+
+import base64
+import logging
+import os
+import re
+
+from mod_pywebsocket import common
+from mod_pywebsocket.extensions import get_extension_processor
+from mod_pywebsocket.handshake._base import check_request_line
+from mod_pywebsocket.handshake._base import format_header
+from mod_pywebsocket.handshake._base import get_mandatory_header
+from mod_pywebsocket.handshake._base import HandshakeException
+from mod_pywebsocket.handshake._base import parse_token_list
+from mod_pywebsocket.handshake._base import validate_mandatory_header
+from mod_pywebsocket.handshake._base import validate_subprotocol
+from mod_pywebsocket.handshake._base import VersionException
+from mod_pywebsocket.stream import Stream
+from mod_pywebsocket.stream import StreamOptions
+from mod_pywebsocket import util
+
+
+# Used to validate the value in the Sec-WebSocket-Key header strictly. RFC 4648
+# disallows non-zero padding, so the character right before == must be any of
+# A, Q, g and w.
+_SEC_WEBSOCKET_KEY_REGEX = re.compile('^[+/0-9A-Za-z]{21}[AQgw]==$')
+
+# Defining aliases for values used frequently.
+_VERSION_HYBI08 = common.VERSION_HYBI08
+_VERSION_HYBI08_STRING = str(_VERSION_HYBI08)
+_VERSION_LATEST = common.VERSION_HYBI_LATEST
+_VERSION_LATEST_STRING = str(_VERSION_LATEST)
+_SUPPORTED_VERSIONS = [
+    _VERSION_LATEST,
+    _VERSION_HYBI08,
+]
+
+
+def compute_accept(key):
+    """Computes value for the Sec-WebSocket-Accept header from value of the
+    Sec-WebSocket-Key header.
+    """
+
+    accept_binary = util.sha1_hash(
+        key + common.WEBSOCKET_ACCEPT_UUID).digest()
+    accept = base64.b64encode(accept_binary)
+
+    return (accept, accept_binary)
+
+
+class Handshaker(object):
+    """Opening handshake processor for the WebSocket protocol (RFC 6455)."""
+
+    def __init__(self, request, dispatcher):
+        """Construct an instance.
+
+        Args:
+            request: mod_python request.
+            dispatcher: Dispatcher (dispatch.Dispatcher).
+
+        Handshaker will add attributes such as ws_resource during handshake.
+        """
+
+        self._logger = util.get_class_logger(self)
+
+        self._request = request
+        self._dispatcher = dispatcher
+
+    def _validate_connection_header(self):
+        connection = get_mandatory_header(
+            self._request, common.CONNECTION_HEADER)
+
+        try:
+            connection_tokens = parse_token_list(connection)
+        except HandshakeException, e:
+            raise HandshakeException(
+                'Failed to parse %s: %s' % (common.CONNECTION_HEADER, e))
+
+        connection_is_valid = False
+        for token in connection_tokens:
+            if token.lower() == common.UPGRADE_CONNECTION_TYPE.lower():
+                connection_is_valid = True
+                break
+        if not connection_is_valid:
+            raise HandshakeException(
+                '%s header doesn\'t contain "%s"' %
+                (common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE))
+
+    def do_handshake(self):
+        self._request.ws_close_code = None
+        self._request.ws_close_reason = None
+
+        # Parsing.
+
+        check_request_line(self._request)
+
+        validate_mandatory_header(
+            self._request,
+            common.UPGRADE_HEADER,
+            common.WEBSOCKET_UPGRADE_TYPE)
+
+        self._validate_connection_header()
+
+        self._request.ws_resource = self._request.uri
+
+        unused_host = get_mandatory_header(self._request, common.HOST_HEADER)
+
+        self._request.ws_version = self._check_version()
+
+        # This handshake must be based on latest hybi. We are responsible to
+        # fallback to HTTP on handshake failure as latest hybi handshake
+        # specifies.
+        try:
+            self._get_origin()
+            self._set_protocol()
+            self._parse_extensions()
+
+            # Key validation, response generation.
+
+            key = self._get_key()
+            (accept, accept_binary) = compute_accept(key)
+            self._logger.debug(
+                '%s: %r (%s)',
+                common.SEC_WEBSOCKET_ACCEPT_HEADER,
+                accept,
+                util.hexify(accept_binary))
+
+            self._logger.debug('Protocol version is RFC 6455')
+
+            # Setup extension processors.
+
+            processors = []
+            if self._request.ws_requested_extensions is not None:
+                for extension_request in self._request.ws_requested_extensions:
+                    processor = get_extension_processor(extension_request)
+                    # Unknown extension requests are just ignored.
+                    if processor is not None:
+                        processors.append(processor)
+            self._request.ws_extension_processors = processors
+
+            # Extra handshake handler may modify/remove processors.
+            self._dispatcher.do_extra_handshake(self._request)
+            processors = filter(lambda processor: processor is not None,
+                                self._request.ws_extension_processors)
+
+            accepted_extensions = []
+
+            # We need to take care of mux extension here. Extensions that
+            # are placed before mux should be applied to logical channels.
+            mux_index = -1
+            for i, processor in enumerate(processors):
+                if processor.name() == common.MUX_EXTENSION:
+                    mux_index = i
+                    break
+            if mux_index >= 0:
+                mux_processor = processors[mux_index]
+                logical_channel_processors = processors[:mux_index]
+                processors = processors[mux_index+1:]
+
+                for processor in logical_channel_processors:
+                    extension_response = processor.get_extension_response()
+                    if extension_response is None:
+                        # Rejected.
+                        continue
+                    accepted_extensions.append(extension_response)
+                # Pass a shallow copy of accepted_extensions as extensions for
+                # logical channels.
+                mux_response = mux_processor.get_extension_response(
+                    self._request, accepted_extensions[:])
+                if mux_response is not None:
+                    accepted_extensions.append(mux_response)
+
+            stream_options = StreamOptions()
+
+            # When there is mux extension, here, |processors| contain only
+            # prosessors for extensions placed after mux.
+            for processor in processors:
+
+                extension_response = processor.get_extension_response()
+                if extension_response is None:
+                    # Rejected.
+                    continue
+
+                accepted_extensions.append(extension_response)
+
+                processor.setup_stream_options(stream_options)
+
+            if len(accepted_extensions) > 0:
+                self._request.ws_extensions = accepted_extensions
+                self._logger.debug(
+                    'Extensions accepted: %r',
+                    map(common.ExtensionParameter.name, accepted_extensions))
+            else:
+                self._request.ws_extensions = None
+
+            self._request.ws_stream = self._create_stream(stream_options)
+
+            if self._request.ws_requested_protocols is not None:
+                if self._request.ws_protocol is None:
+                    raise HandshakeException(
+                        'do_extra_handshake must choose one subprotocol from '
+                        'ws_requested_protocols and set it to ws_protocol')
+                validate_subprotocol(self._request.ws_protocol, hixie=False)
+
+                self._logger.debug(
+                    'Subprotocol accepted: %r',
+                    self._request.ws_protocol)
+            else:
+                if self._request.ws_protocol is not None:
+                    raise HandshakeException(
+                        'ws_protocol must be None when the client didn\'t '
+                        'request any subprotocol')
+
+            self._send_handshake(accept)
+        except HandshakeException, e:
+            if not e.status:
+                # Fallback to 400 bad request by default.
+                e.status = common.HTTP_STATUS_BAD_REQUEST
+            raise e
+
+    def _get_origin(self):
+        if self._request.ws_version is _VERSION_HYBI08:
+            origin_header = common.SEC_WEBSOCKET_ORIGIN_HEADER
+        else:
+            origin_header = common.ORIGIN_HEADER
+        origin = self._request.headers_in.get(origin_header)
+        if origin is None:
+            self._logger.debug('Client request does not have origin header')
+        self._request.ws_origin = origin
+
+    def _check_version(self):
+        version = get_mandatory_header(self._request,
+                                       common.SEC_WEBSOCKET_VERSION_HEADER)
+        if version == _VERSION_HYBI08_STRING:
+            return _VERSION_HYBI08
+        if version == _VERSION_LATEST_STRING:
+            return _VERSION_LATEST
+
+        if version.find(',') >= 0:
+            raise HandshakeException(
+                'Multiple versions (%r) are not allowed for header %s' %
+                (version, common.SEC_WEBSOCKET_VERSION_HEADER),
+                status=common.HTTP_STATUS_BAD_REQUEST)
+        raise VersionException(
+            'Unsupported version %r for header %s' %
+            (version, common.SEC_WEBSOCKET_VERSION_HEADER),
+            supported_versions=', '.join(map(str, _SUPPORTED_VERSIONS)))
+
+    def _set_protocol(self):
+        self._request.ws_protocol = None
+
+        protocol_header = self._request.headers_in.get(
+            common.SEC_WEBSOCKET_PROTOCOL_HEADER)
+
+        if protocol_header is None:
+            self._request.ws_requested_protocols = None
+            return
+
+        self._request.ws_requested_protocols = parse_token_list(
+            protocol_header)
+        self._logger.debug('Subprotocols requested: %r',
+                           self._request.ws_requested_protocols)
+
+    def _parse_extensions(self):
+        extensions_header = self._request.headers_in.get(
+            common.SEC_WEBSOCKET_EXTENSIONS_HEADER)
+        if not extensions_header:
+            self._request.ws_requested_extensions = None
+            return
+
+        if self._request.ws_version is common.VERSION_HYBI08:
+            allow_quoted_string=False
+        else:
+            allow_quoted_string=True
+        try:
+            self._request.ws_requested_extensions = common.parse_extensions(
+                extensions_header, allow_quoted_string=allow_quoted_string)
+        except common.ExtensionParsingException, e:
+            raise HandshakeException(
+                'Failed to parse Sec-WebSocket-Extensions header: %r' % e)
+
+        self._logger.debug(
+            'Extensions requested: %r',
+            map(common.ExtensionParameter.name,
+                self._request.ws_requested_extensions))
+
+    def _validate_key(self, key):
+        if key.find(',') >= 0:
+            raise HandshakeException('Request has multiple %s header lines or '
+                                     'contains illegal character \',\': %r' %
+                                     (common.SEC_WEBSOCKET_KEY_HEADER, key))
+
+        # Validate
+        key_is_valid = False
+        try:
+            # Validate key by quick regex match before parsing by base64
+            # module. Because base64 module skips invalid characters, we have
+            # to do this in advance to make this server strictly reject illegal
+            # keys.
+            if _SEC_WEBSOCKET_KEY_REGEX.match(key):
+                decoded_key = base64.b64decode(key)
+                if len(decoded_key) == 16:
+                    key_is_valid = True
+        except TypeError, e:
+            pass
+
+        if not key_is_valid:
+            raise HandshakeException(
+                'Illegal value for header %s: %r' %
+                (common.SEC_WEBSOCKET_KEY_HEADER, key))
+
+        return decoded_key
+
+    def _get_key(self):
+        key = get_mandatory_header(
+            self._request, common.SEC_WEBSOCKET_KEY_HEADER)
+
+        decoded_key = self._validate_key(key)
+
+        self._logger.debug(
+            '%s: %r (%s)',
+            common.SEC_WEBSOCKET_KEY_HEADER,
+            key,
+            util.hexify(decoded_key))
+
+        return key
+
+    def _create_stream(self, stream_options):
+        return Stream(self._request, stream_options)
+
+    def _create_handshake_response(self, accept):
+        response = []
+
+        response.append('HTTP/1.1 101 Switching Protocols\r\n')
+
+        response.append(format_header(
+            common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE))
+        response.append(format_header(
+            common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE))
+        response.append(format_header(
+            common.SEC_WEBSOCKET_ACCEPT_HEADER, accept))
+        if self._request.ws_protocol is not None:
+            response.append(format_header(
+                common.SEC_WEBSOCKET_PROTOCOL_HEADER,
+                self._request.ws_protocol))
+        if (self._request.ws_extensions is not None and
+            len(self._request.ws_extensions) != 0):
+            response.append(format_header(
+                common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
+                common.format_extensions(self._request.ws_extensions)))
+        response.append('\r\n')
+
+        return ''.join(response)
+
+    def _send_handshake(self, accept):
+        raw_response = self._create_handshake_response(accept)
+        self._request.connection.write(raw_response)
+        self._logger.debug('Sent server\'s opening handshake: %r',
+                           raw_response)
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/hybi00.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/hybi00.py
new file mode 100644
index 0000000..cc6f8dc
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/hybi00.py
@@ -0,0 +1,242 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""This file provides the opening handshake processor for the WebSocket
+protocol version HyBi 00.
+
+Specification:
+http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00
+"""
+
+
+# Note: request.connection.write/read are used in this module, even though
+# mod_python document says that they should be used only in connection
+# handlers. Unfortunately, we have no other options. For example,
+# request.write/read are not suitable because they don't allow direct raw bytes
+# writing/reading.
+
+
+import logging
+import re
+import struct
+
+from mod_pywebsocket import common
+from mod_pywebsocket.stream import StreamHixie75
+from mod_pywebsocket import util
+from mod_pywebsocket.handshake._base import HandshakeException
+from mod_pywebsocket.handshake._base import build_location
+from mod_pywebsocket.handshake._base import check_header_lines
+from mod_pywebsocket.handshake._base import format_header
+from mod_pywebsocket.handshake._base import get_mandatory_header
+from mod_pywebsocket.handshake._base import validate_subprotocol
+
+
+_MANDATORY_HEADERS = [
+    # key, expected value or None
+    [common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75],
+    [common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE],
+]
+
+
+class Handshaker(object):
+    """Opening handshake processor for the WebSocket protocol version HyBi 00.
+    """
+
+    def __init__(self, request, dispatcher):
+        """Construct an instance.
+
+        Args:
+            request: mod_python request.
+            dispatcher: Dispatcher (dispatch.Dispatcher).
+
+        Handshaker will add attributes such as ws_resource in performing
+        handshake.
+        """
+
+        self._logger = util.get_class_logger(self)
+
+        self._request = request
+        self._dispatcher = dispatcher
+
+    def do_handshake(self):
+        """Perform WebSocket Handshake.
+
+        On _request, we set
+            ws_resource, ws_protocol, ws_location, ws_origin, ws_challenge,
+            ws_challenge_md5: WebSocket handshake information.
+            ws_stream: Frame generation/parsing class.
+            ws_version: Protocol version.
+
+        Raises:
+            HandshakeException: when any error happened in parsing the opening
+                                handshake request.
+        """
+
+        # 5.1 Reading the client's opening handshake.
+        # dispatcher sets it in self._request.
+        check_header_lines(self._request, _MANDATORY_HEADERS)
+        self._set_resource()
+        self._set_subprotocol()
+        self._set_location()
+        self._set_origin()
+        self._set_challenge_response()
+        self._set_protocol_version()
+
+        self._dispatcher.do_extra_handshake(self._request)
+
+        self._send_handshake()
+
+    def _set_resource(self):
+        self._request.ws_resource = self._request.uri
+
+    def _set_subprotocol(self):
+        # |Sec-WebSocket-Protocol|
+        subprotocol = self._request.headers_in.get(
+            common.SEC_WEBSOCKET_PROTOCOL_HEADER)
+        if subprotocol is not None:
+            validate_subprotocol(subprotocol, hixie=True)
+        self._request.ws_protocol = subprotocol
+
+    def _set_location(self):
+        # |Host|
+        host = self._request.headers_in.get(common.HOST_HEADER)
+        if host is not None:
+            self._request.ws_location = build_location(self._request)
+        # TODO(ukai): check host is this host.
+
+    def _set_origin(self):
+        # |Origin|
+        origin = self._request.headers_in.get(common.ORIGIN_HEADER)
+        if origin is not None:
+            self._request.ws_origin = origin
+
+    def _set_protocol_version(self):
+        # |Sec-WebSocket-Draft|
+        draft = self._request.headers_in.get(common.SEC_WEBSOCKET_DRAFT_HEADER)
+        if draft is not None and draft != '0':
+            raise HandshakeException('Illegal value for %s: %s' %
+                                     (common.SEC_WEBSOCKET_DRAFT_HEADER,
+                                      draft))
+
+        self._logger.debug('Protocol version is HyBi 00')
+        self._request.ws_version = common.VERSION_HYBI00
+        self._request.ws_stream = StreamHixie75(self._request, True)
+
+    def _set_challenge_response(self):
+        # 5.2 4-8.
+        self._request.ws_challenge = self._get_challenge()
+        # 5.2 9. let /response/ be the MD5 finterprint of /challenge/
+        self._request.ws_challenge_md5 = util.md5_hash(
+            self._request.ws_challenge).digest()
+        self._logger.debug(
+            'Challenge: %r (%s)',
+            self._request.ws_challenge,
+            util.hexify(self._request.ws_challenge))
+        self._logger.debug(
+            'Challenge response: %r (%s)',
+            self._request.ws_challenge_md5,
+            util.hexify(self._request.ws_challenge_md5))
+
+    def _get_key_value(self, key_field):
+        key_value = get_mandatory_header(self._request, key_field)
+
+        self._logger.debug('%s: %r', key_field, key_value)
+
+        # 5.2 4. let /key-number_n/ be the digits (characters in the range
+        # U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9)) in /key_n/,
+        # interpreted as a base ten integer, ignoring all other characters
+        # in /key_n/.
+        try:
+            key_number = int(re.sub("\\D", "", key_value))
+        except:
+            raise HandshakeException('%s field contains no digit' % key_field)
+        # 5.2 5. let /spaces_n/ be the number of U+0020 SPACE characters
+        # in /key_n/.
+        spaces = re.subn(" ", "", key_value)[1]
+        if spaces == 0:
+            raise HandshakeException('%s field contains no space' % key_field)
+
+        self._logger.debug(
+            '%s: Key-number is %d and number of spaces is %d',
+            key_field, key_number, spaces)
+
+        # 5.2 6. if /key-number_n/ is not an integral multiple of /spaces_n/
+        # then abort the WebSocket connection.
+        if key_number % spaces != 0:
+            raise HandshakeException(
+                '%s: Key-number (%d) is not an integral multiple of spaces '
+                '(%d)' % (key_field, key_number, spaces))
+        # 5.2 7. let /part_n/ be /key-number_n/ divided by /spaces_n/.
+        part = key_number / spaces
+        self._logger.debug('%s: Part is %d', key_field, part)
+        return part
+
+    def _get_challenge(self):
+        # 5.2 4-7.
+        key1 = self._get_key_value(common.SEC_WEBSOCKET_KEY1_HEADER)
+        key2 = self._get_key_value(common.SEC_WEBSOCKET_KEY2_HEADER)
+        # 5.2 8. let /challenge/ be the concatenation of /part_1/,
+        challenge = ''
+        challenge += struct.pack('!I', key1)  # network byteorder int
+        challenge += struct.pack('!I', key2)  # network byteorder int
+        challenge += self._request.connection.read(8)
+        return challenge
+
+    def _send_handshake(self):
+        response = []
+
+        # 5.2 10. send the following line.
+        response.append('HTTP/1.1 101 WebSocket Protocol Handshake\r\n')
+
+        # 5.2 11. send the following fields to the client.
+        response.append(format_header(
+            common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75))
+        response.append(format_header(
+            common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE))
+        response.append(format_header(
+            common.SEC_WEBSOCKET_LOCATION_HEADER, self._request.ws_location))
+        response.append(format_header(
+            common.SEC_WEBSOCKET_ORIGIN_HEADER, self._request.ws_origin))
+        if self._request.ws_protocol:
+            response.append(format_header(
+                common.SEC_WEBSOCKET_PROTOCOL_HEADER,
+                self._request.ws_protocol))
+        # 5.2 12. send two bytes 0x0D 0x0A.
+        response.append('\r\n')
+        # 5.2 13. send /response/
+        response.append(self._request.ws_challenge_md5)
+
+        raw_response = ''.join(response)
+        self._request.connection.write(raw_response)
+        self._logger.debug('Sent server\'s opening handshake: %r',
+                           raw_response)
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/headerparserhandler.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/headerparserhandler.py
new file mode 100644
index 0000000..2cc62de
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/headerparserhandler.py
@@ -0,0 +1,244 @@
+# Copyright 2011, 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.
+
+
+"""PythonHeaderParserHandler for mod_pywebsocket.
+
+Apache HTTP Server and mod_python must be configured such that this
+function is called to handle WebSocket request.
+"""
+
+
+import logging
+
+from mod_python import apache
+
+from mod_pywebsocket import common
+from mod_pywebsocket import dispatch
+from mod_pywebsocket import handshake
+from mod_pywebsocket import util
+
+
+# PythonOption to specify the handler root directory.
+_PYOPT_HANDLER_ROOT = 'mod_pywebsocket.handler_root'
+
+# PythonOption to specify the handler scan directory.
+# This must be a directory under the root directory.
+# The default is the root directory.
+_PYOPT_HANDLER_SCAN = 'mod_pywebsocket.handler_scan'
+
+# PythonOption to allow handlers whose canonical path is
+# not under the root directory. It's disallowed by default.
+# Set this option with value of 'yes' to allow.
+_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT = (
+    'mod_pywebsocket.allow_handlers_outside_root_dir')
+# Map from values to their meanings. 'Yes' and 'No' are allowed just for
+# compatibility.
+_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT_DEFINITION = {
+    'off': False, 'no': False, 'on': True, 'yes': True}
+
+# (Obsolete option. Ignored.)
+# PythonOption to specify to allow handshake defined in Hixie 75 version
+# protocol. The default is None (Off)
+_PYOPT_ALLOW_DRAFT75 = 'mod_pywebsocket.allow_draft75'
+# Map from values to their meanings.
+_PYOPT_ALLOW_DRAFT75_DEFINITION = {'off': False, 'on': True}
+
+
+class ApacheLogHandler(logging.Handler):
+    """Wrapper logging.Handler to emit log message to apache's error.log."""
+
+    _LEVELS = {
+        logging.DEBUG: apache.APLOG_DEBUG,
+        logging.INFO: apache.APLOG_INFO,
+        logging.WARNING: apache.APLOG_WARNING,
+        logging.ERROR: apache.APLOG_ERR,
+        logging.CRITICAL: apache.APLOG_CRIT,
+        }
+
+    def __init__(self, request=None):
+        logging.Handler.__init__(self)
+        self._log_error = apache.log_error
+        if request is not None:
+            self._log_error = request.log_error
+
+        # Time and level will be printed by Apache.
+        self._formatter = logging.Formatter('%(name)s: %(message)s')
+
+    def emit(self, record):
+        apache_level = apache.APLOG_DEBUG
+        if record.levelno in ApacheLogHandler._LEVELS:
+            apache_level = ApacheLogHandler._LEVELS[record.levelno]
+
+        msg = self._formatter.format(record)
+
+        # "server" parameter must be passed to have "level" parameter work.
+        # If only "level" parameter is passed, nothing shows up on Apache's
+        # log. However, at this point, we cannot get the server object of the
+        # virtual host which will process WebSocket requests. The only server
+        # object we can get here is apache.main_server. But Wherever (server
+        # configuration context or virtual host context) we put
+        # PythonHeaderParserHandler directive, apache.main_server just points
+        # the main server instance (not any of virtual server instance). Then,
+        # Apache follows LogLevel directive in the server configuration context
+        # to filter logs. So, we need to specify LogLevel in the server
+        # configuration context. Even if we specify "LogLevel debug" in the
+        # virtual host context which actually handles WebSocket connections,
+        # DEBUG level logs never show up unless "LogLevel debug" is specified
+        # in the server configuration context.
+        #
+        # TODO(tyoshino): Provide logging methods on request object. When
+        # request is mp_request object (when used together with Apache), the
+        # methods call request.log_error indirectly. When request is
+        # _StandaloneRequest, the methods call Python's logging facility which
+        # we create in standalone.py.
+        self._log_error(msg, apache_level, apache.main_server)
+
+
+def _configure_logging():
+    logger = logging.getLogger()
+    # Logs are filtered by Apache based on LogLevel directive in Apache
+    # configuration file. We must just pass logs for all levels to
+    # ApacheLogHandler.
+    logger.setLevel(logging.DEBUG)
+    logger.addHandler(ApacheLogHandler())
+
+
+_configure_logging()
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def _parse_option(name, value, definition):
+    if value is None:
+        return False
+
+    meaning = definition.get(value.lower())
+    if meaning is None:
+        raise Exception('Invalid value for PythonOption %s: %r' %
+                        (name, value))
+    return meaning
+
+
+def _create_dispatcher():
+    _LOGGER.info('Initializing Dispatcher')
+
+    options = apache.main_server.get_options()
+
+    handler_root = options.get(_PYOPT_HANDLER_ROOT, None)
+    if not handler_root:
+        raise Exception('PythonOption %s is not defined' % _PYOPT_HANDLER_ROOT,
+                        apache.APLOG_ERR)
+
+    handler_scan = options.get(_PYOPT_HANDLER_SCAN, handler_root)
+
+    allow_handlers_outside_root = _parse_option(
+        _PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT,
+        options.get(_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT),
+        _PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT_DEFINITION)
+
+    dispatcher = dispatch.Dispatcher(
+        handler_root, handler_scan, allow_handlers_outside_root)
+
+    for warning in dispatcher.source_warnings():
+        apache.log_error('mod_pywebsocket: %s' % warning, apache.APLOG_WARNING)
+
+    return dispatcher
+
+
+# Initialize
+_dispatcher = _create_dispatcher()
+
+
+def headerparserhandler(request):
+    """Handle request.
+
+    Args:
+        request: mod_python request.
+
+    This function is named headerparserhandler because it is the default
+    name for a PythonHeaderParserHandler.
+    """
+
+    handshake_is_done = False
+    try:
+        # Fallback to default http handler for request paths for which
+        # we don't have request handlers.
+        if not _dispatcher.get_handler_suite(request.uri):
+            request.log_error('No handler for resource: %r' % request.uri,
+                              apache.APLOG_INFO)
+            request.log_error('Fallback to Apache', apache.APLOG_INFO)
+            return apache.DECLINED
+    except dispatch.DispatchException, e:
+        request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_INFO)
+        if not handshake_is_done:
+            return e.status
+
+    try:
+        allow_draft75 = _parse_option(
+            _PYOPT_ALLOW_DRAFT75,
+            apache.main_server.get_options().get(_PYOPT_ALLOW_DRAFT75),
+            _PYOPT_ALLOW_DRAFT75_DEFINITION)
+
+        try:
+            handshake.do_handshake(
+                request, _dispatcher, allowDraft75=allow_draft75)
+        except handshake.VersionException, e:
+            request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_INFO)
+            request.err_headers_out.add(common.SEC_WEBSOCKET_VERSION_HEADER,
+                                        e.supported_versions)
+            return apache.HTTP_BAD_REQUEST
+        except handshake.HandshakeException, e:
+            # Handshake for ws/wss failed.
+            # Send http response with error status.
+            request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_INFO)
+            return e.status
+
+        handshake_is_done = True
+        request._dispatcher = _dispatcher
+        _dispatcher.transfer_data(request)
+    except handshake.AbortedByUserException, e:
+        request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_INFO)
+    except Exception, e:
+        # DispatchException can also be thrown if something is wrong in
+        # pywebsocket code. It's caught here, then.
+
+        request.log_error('mod_pywebsocket: %s\n%s' %
+                          (e, util.get_stack_trace()),
+                          apache.APLOG_ERR)
+        # Unknown exceptions before handshake mean Apache must handle its
+        # request with another handler.
+        if not handshake_is_done:
+            return apache.DECLINED
+    # Set assbackwards to suppress response header generation by Apache.
+    request.assbackwards = 1
+    return apache.DONE  # Return DONE such that no other handlers are invoked.
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/http_header_util.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/http_header_util.py
new file mode 100644
index 0000000..b774653
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/http_header_util.py
@@ -0,0 +1,263 @@
+# Copyright 2011, 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.
+
+
+"""Utilities for parsing and formatting headers that follow the grammar defined
+in HTTP RFC http://www.ietf.org/rfc/rfc2616.txt.
+"""
+
+
+import urlparse
+
+
+_SEPARATORS = '()<>@,;:\\"/[]?={} \t'
+
+
+def _is_char(c):
+    """Returns true iff c is in CHAR as specified in HTTP RFC."""
+
+    return ord(c) <= 127
+
+
+def _is_ctl(c):
+    """Returns true iff c is in CTL as specified in HTTP RFC."""
+
+    return ord(c) <= 31 or ord(c) == 127
+
+
+class ParsingState(object):
+
+    def __init__(self, data):
+        self.data = data
+        self.head = 0
+
+
+def peek(state, pos=0):
+    """Peeks the character at pos from the head of data."""
+
+    if state.head + pos >= len(state.data):
+        return None
+
+    return state.data[state.head + pos]
+
+
+def consume(state, amount=1):
+    """Consumes specified amount of bytes from the head and returns the
+    consumed bytes. If there's not enough bytes to consume, returns None.
+    """
+
+    if state.head + amount > len(state.data):
+        return None
+
+    result = state.data[state.head:state.head + amount]
+    state.head = state.head + amount
+    return result
+
+
+def consume_string(state, expected):
+    """Given a parsing state and a expected string, consumes the string from
+    the head. Returns True if consumed successfully. Otherwise, returns
+    False.
+    """
+
+    pos = 0
+
+    for c in expected:
+        if c != peek(state, pos):
+            return False
+        pos += 1
+
+    consume(state, pos)
+    return True
+
+
+def consume_lws(state):
+    """Consumes a LWS from the head. Returns True if any LWS is consumed.
+    Otherwise, returns False.
+
+    LWS = [CRLF] 1*( SP | HT )
+    """
+
+    original_head = state.head
+
+    consume_string(state, '\r\n')
+
+    pos = 0
+
+    while True:
+        c = peek(state, pos)
+        if c == ' ' or c == '\t':
+            pos += 1
+        else:
+            if pos == 0:
+                state.head = original_head
+                return False
+            else:
+                consume(state, pos)
+                return True
+
+
+def consume_lwses(state):
+    """Consumes *LWS from the head."""
+
+    while consume_lws(state):
+        pass
+
+
+def consume_token(state):
+    """Consumes a token from the head. Returns the token or None if no token
+    was found.
+    """
+
+    pos = 0
+
+    while True:
+        c = peek(state, pos)
+        if c is None or c in _SEPARATORS or _is_ctl(c) or not _is_char(c):
+            if pos == 0:
+                return None
+
+            return consume(state, pos)
+        else:
+            pos += 1
+
+
+def consume_token_or_quoted_string(state):
+    """Consumes a token or a quoted-string, and returns the token or unquoted
+    string. If no token or quoted-string was found, returns None.
+    """
+
+    original_head = state.head
+
+    if not consume_string(state, '"'):
+        return consume_token(state)
+
+    result = []
+
+    expect_quoted_pair = False
+
+    while True:
+        if not expect_quoted_pair and consume_lws(state):
+            result.append(' ')
+            continue
+
+        c = consume(state)
+        if c is None:
+            # quoted-string is not enclosed with double quotation
+            state.head = original_head
+            return None
+        elif expect_quoted_pair:
+            expect_quoted_pair = False
+            if _is_char(c):
+                result.append(c)
+            else:
+                # Non CHAR character found in quoted-pair
+                state.head = original_head
+                return None
+        elif c == '\\':
+            expect_quoted_pair = True
+        elif c == '"':
+            return ''.join(result)
+        elif _is_ctl(c):
+            # Invalid character %r found in qdtext
+            state.head = original_head
+            return None
+        else:
+            result.append(c)
+
+
+def quote_if_necessary(s):
+    """Quotes arbitrary string into quoted-string."""
+
+    quote = False
+    if s == '':
+        return '""'
+
+    result = []
+    for c in s:
+        if c == '"' or c in _SEPARATORS or _is_ctl(c) or not _is_char(c):
+            quote = True
+
+        if c == '"' or _is_ctl(c):
+            result.append('\\' + c)
+        else:
+            result.append(c)
+
+    if quote:
+        return '"' + ''.join(result) + '"'
+    else:
+        return ''.join(result)
+
+
+def parse_uri(uri):
+    """Parse absolute URI then return host, port and resource."""
+
+    parsed = urlparse.urlsplit(uri)
+    if parsed.scheme != 'wss' and parsed.scheme != 'ws':
+        # |uri| must be a relative URI.
+        # TODO(toyoshim): Should validate |uri|.
+        return None, None, uri
+
+    if parsed.hostname is None:
+        return None, None, None
+
+    port = None
+    try:
+        port = parsed.port
+    except ValueError, e:
+        # port property cause ValueError on invalid null port description like
+        # 'ws://host:/path'.
+        return None, None, None
+
+    if port is None:
+        if parsed.scheme == 'ws':
+            port = 80
+        else:
+            port = 443
+
+    path = parsed.path
+    if not path:
+        path += '/'
+    if parsed.query:
+        path += '?' + parsed.query
+    if parsed.fragment:
+        path += '#' + parsed.fragment
+
+    return parsed.hostname, port, path
+
+
+try:
+    urlparse.uses_netloc.index('ws')
+except ValueError, e:
+    # urlparse in Python2.5.1 doesn't have 'ws' and 'wss' entries.
+    urlparse.uses_netloc.append('ws')
+    urlparse.uses_netloc.append('wss')
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/memorizingfile.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/memorizingfile.py
new file mode 100644
index 0000000..4d4cd95
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/memorizingfile.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, 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.
+
+
+"""Memorizing file.
+
+A memorizing file wraps a file and memorizes lines read by readline.
+"""
+
+
+import sys
+
+
+class MemorizingFile(object):
+    """MemorizingFile wraps a file and memorizes lines read by readline.
+
+    Note that data read by other methods are not memorized. This behavior
+    is good enough for memorizing lines SimpleHTTPServer reads before
+    the control reaches WebSocketRequestHandler.
+    """
+
+    def __init__(self, file_, max_memorized_lines=sys.maxint):
+        """Construct an instance.
+
+        Args:
+            file_: the file object to wrap.
+            max_memorized_lines: the maximum number of lines to memorize.
+                Only the first max_memorized_lines are memorized.
+                Default: sys.maxint.
+        """
+
+        self._file = file_
+        self._memorized_lines = []
+        self._max_memorized_lines = max_memorized_lines
+        self._buffered = False
+        self._buffered_line = None
+
+    def __getattribute__(self, name):
+        if name in ('_file', '_memorized_lines', '_max_memorized_lines',
+                    '_buffered', '_buffered_line', 'readline',
+                    'get_memorized_lines'):
+            return object.__getattribute__(self, name)
+        return self._file.__getattribute__(name)
+
+    def readline(self, size=-1):
+        """Override file.readline and memorize the line read.
+
+        Note that even if size is specified and smaller than actual size,
+        the whole line will be read out from underlying file object by
+        subsequent readline calls.
+        """
+
+        if self._buffered:
+            line = self._buffered_line
+            self._buffered = False
+        else:
+            line = self._file.readline()
+            if line and len(self._memorized_lines) < self._max_memorized_lines:
+                self._memorized_lines.append(line)
+        if size >= 0 and size < len(line):
+            self._buffered = True
+            self._buffered_line = line[size:]
+            return line[:size]
+        return line
+
+    def get_memorized_lines(self):
+        """Get lines memorized so far."""
+        return self._memorized_lines
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/msgutil.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/msgutil.py
new file mode 100644
index 0000000..4c1a011
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/msgutil.py
@@ -0,0 +1,219 @@
+# Copyright 2011, 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.
+
+
+"""Message related utilities.
+
+Note: request.connection.write/read are used in this module, even though
+mod_python document says that they should be used only in connection
+handlers. Unfortunately, we have no other options. For example,
+request.write/read are not suitable because they don't allow direct raw
+bytes writing/reading.
+"""
+
+
+import Queue
+import threading
+
+
+# Export Exception symbols from msgutil for backward compatibility
+from mod_pywebsocket._stream_base import ConnectionTerminatedException
+from mod_pywebsocket._stream_base import InvalidFrameException
+from mod_pywebsocket._stream_base import BadOperationException
+from mod_pywebsocket._stream_base import UnsupportedFrameException
+
+
+# An API for handler to send/receive WebSocket messages.
+def close_connection(request):
+    """Close connection.
+
+    Args:
+        request: mod_python request.
+    """
+    request.ws_stream.close_connection()
+
+
+def send_message(request, payload_data, end=True, binary=False):
+    """Send a message (or part of a message).
+
+    Args:
+        request: mod_python request.
+        payload_data: unicode text or str binary to send.
+        end: True to terminate a message.
+             False to send payload_data as part of a message that is to be
+             terminated by next or later send_message call with end=True.
+        binary: send payload_data as binary frame(s).
+    Raises:
+        BadOperationException: when server already terminated.
+    """
+    request.ws_stream.send_message(payload_data, end, binary)
+
+
+def receive_message(request):
+    """Receive a WebSocket frame and return its payload as a text in
+    unicode or a binary in str.
+
+    Args:
+        request: mod_python request.
+    Raises:
+        InvalidFrameException:     when client send invalid frame.
+        UnsupportedFrameException: when client send unsupported frame e.g. some
+                                   of reserved bit is set but no extension can
+                                   recognize it.
+        InvalidUTF8Exception:      when client send a text frame containing any
+                                   invalid UTF-8 string.
+        ConnectionTerminatedException: when the connection is closed
+                                   unexpectedly.
+        BadOperationException:     when client already terminated.
+    """
+    return request.ws_stream.receive_message()
+
+
+def send_ping(request, body=''):
+    request.ws_stream.send_ping(body)
+
+
+class MessageReceiver(threading.Thread):
+    """This class receives messages from the client.
+
+    This class provides three ways to receive messages: blocking,
+    non-blocking, and via callback. Callback has the highest precedence.
+
+    Note: This class should not be used with the standalone server for wss
+    because pyOpenSSL used by the server raises a fatal error if the socket
+    is accessed from multiple threads.
+    """
+
+    def __init__(self, request, onmessage=None):
+        """Construct an instance.
+
+        Args:
+            request: mod_python request.
+            onmessage: a function to be called when a message is received.
+                       May be None. If not None, the function is called on
+                       another thread. In that case, MessageReceiver.receive
+                       and MessageReceiver.receive_nowait are useless
+                       because they will never return any messages.
+        """
+
+        threading.Thread.__init__(self)
+        self._request = request
+        self._queue = Queue.Queue()
+        self._onmessage = onmessage
+        self._stop_requested = False
+        self.setDaemon(True)
+        self.start()
+
+    def run(self):
+        try:
+            while not self._stop_requested:
+                message = receive_message(self._request)
+                if self._onmessage:
+                    self._onmessage(message)
+                else:
+                    self._queue.put(message)
+        finally:
+            close_connection(self._request)
+
+    def receive(self):
+        """ Receive a message from the channel, blocking.
+
+        Returns:
+            message as a unicode string.
+        """
+        return self._queue.get()
+
+    def receive_nowait(self):
+        """ Receive a message from the channel, non-blocking.
+
+        Returns:
+            message as a unicode string if available. None otherwise.
+        """
+        try:
+            message = self._queue.get_nowait()
+        except Queue.Empty:
+            message = None
+        return message
+
+    def stop(self):
+        """Request to stop this instance.
+
+        The instance will be stopped after receiving the next message.
+        This method may not be very useful, but there is no clean way
+        in Python to forcefully stop a running thread.
+        """
+        self._stop_requested = True
+
+
+class MessageSender(threading.Thread):
+    """This class sends messages to the client.
+
+    This class provides both synchronous and asynchronous ways to send
+    messages.
+
+    Note: This class should not be used with the standalone server for wss
+    because pyOpenSSL used by the server raises a fatal error if the socket
+    is accessed from multiple threads.
+    """
+
+    def __init__(self, request):
+        """Construct an instance.
+
+        Args:
+            request: mod_python request.
+        """
+        threading.Thread.__init__(self)
+        self._request = request
+        self._queue = Queue.Queue()
+        self.setDaemon(True)
+        self.start()
+
+    def run(self):
+        while True:
+            message, condition = self._queue.get()
+            condition.acquire()
+            send_message(self._request, message)
+            condition.notify()
+            condition.release()
+
+    def send(self, message):
+        """Send a message, blocking."""
+
+        condition = threading.Condition()
+        condition.acquire()
+        self._queue.put((message, condition))
+        condition.wait()
+
+    def send_nowait(self, message):
+        """Send a message, non-blocking."""
+
+        self._queue.put((message, threading.Condition()))
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/mux.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/mux.py
new file mode 100644
index 0000000..f0bdd24
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/mux.py
@@ -0,0 +1,1636 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""This file provides classes and helper functions for multiplexing extension.
+
+Specification:
+http://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-06
+"""
+
+
+import collections
+import copy
+import email
+import email.parser
+import logging
+import math
+import struct
+import threading
+import traceback
+
+from mod_pywebsocket import common
+from mod_pywebsocket import handshake
+from mod_pywebsocket import util
+from mod_pywebsocket._stream_base import BadOperationException
+from mod_pywebsocket._stream_base import ConnectionTerminatedException
+from mod_pywebsocket._stream_hybi import Frame
+from mod_pywebsocket._stream_hybi import Stream
+from mod_pywebsocket._stream_hybi import StreamOptions
+from mod_pywebsocket._stream_hybi import create_binary_frame
+from mod_pywebsocket._stream_hybi import create_closing_handshake_body
+from mod_pywebsocket._stream_hybi import create_header
+from mod_pywebsocket._stream_hybi import create_length_header
+from mod_pywebsocket._stream_hybi import parse_frame
+from mod_pywebsocket.handshake import hybi
+
+
+_CONTROL_CHANNEL_ID = 0
+_DEFAULT_CHANNEL_ID = 1
+
+_MUX_OPCODE_ADD_CHANNEL_REQUEST = 0
+_MUX_OPCODE_ADD_CHANNEL_RESPONSE = 1
+_MUX_OPCODE_FLOW_CONTROL = 2
+_MUX_OPCODE_DROP_CHANNEL = 3
+_MUX_OPCODE_NEW_CHANNEL_SLOT = 4
+
+_MAX_CHANNEL_ID = 2 ** 29 - 1
+
+_INITIAL_NUMBER_OF_CHANNEL_SLOTS = 64
+_INITIAL_QUOTA_FOR_CLIENT = 8 * 1024
+
+_HANDSHAKE_ENCODING_IDENTITY = 0
+_HANDSHAKE_ENCODING_DELTA = 1
+
+# We need only these status code for now.
+_HTTP_BAD_RESPONSE_MESSAGES = {
+    common.HTTP_STATUS_BAD_REQUEST: 'Bad Request',
+}
+
+# DropChannel reason code
+# TODO(bashi): Define all reason code defined in -05 draft.
+_DROP_CODE_NORMAL_CLOSURE = 1000
+
+_DROP_CODE_INVALID_ENCAPSULATING_MESSAGE = 2001
+_DROP_CODE_CHANNEL_ID_TRUNCATED = 2002
+_DROP_CODE_ENCAPSULATED_FRAME_IS_TRUNCATED = 2003
+_DROP_CODE_UNKNOWN_MUX_OPCODE = 2004
+_DROP_CODE_INVALID_MUX_CONTROL_BLOCK = 2005
+_DROP_CODE_CHANNEL_ALREADY_EXISTS = 2006
+_DROP_CODE_NEW_CHANNEL_SLOT_VIOLATION = 2007
+
+_DROP_CODE_UNKNOWN_REQUEST_ENCODING = 3002
+_DROP_CODE_SEND_QUOTA_VIOLATION = 3005
+_DROP_CODE_ACKNOWLEDGED = 3008
+
+
+class MuxUnexpectedException(Exception):
+    """Exception in handling multiplexing extension."""
+    pass
+
+
+# Temporary
+class MuxNotImplementedException(Exception):
+    """Raised when a flow enters unimplemented code path."""
+    pass
+
+
+class LogicalConnectionClosedException(Exception):
+    """Raised when logical connection is gracefully closed."""
+    pass
+
+
+class PhysicalConnectionError(Exception):
+    """Raised when there is a physical connection error."""
+    def __init__(self, drop_code, message=''):
+        super(PhysicalConnectionError, self).__init__(
+            'code=%d, message=%r' % (drop_code, message))
+        self.drop_code = drop_code
+        self.message = message
+
+
+class LogicalChannelError(Exception):
+    """Raised when there is a logical channel error."""
+    def __init__(self, channel_id, drop_code, message=''):
+        super(LogicalChannelError, self).__init__(
+            'channel_id=%d, code=%d, message=%r' % (
+                channel_id, drop_code, message))
+        self.channel_id = channel_id
+        self.drop_code = drop_code
+        self.message = message
+
+
+def _encode_channel_id(channel_id):
+    if channel_id < 0:
+        raise ValueError('Channel id %d must not be negative' % channel_id)
+
+    if channel_id < 2 ** 7:
+        return chr(channel_id)
+    if channel_id < 2 ** 14:
+        return struct.pack('!H', 0x8000 + channel_id)
+    if channel_id < 2 ** 21:
+        first = chr(0xc0 + (channel_id >> 16))
+        return first + struct.pack('!H', channel_id & 0xffff)
+    if channel_id < 2 ** 29:
+        return struct.pack('!L', 0xe0000000 + channel_id)
+
+    raise ValueError('Channel id %d is too large' % channel_id)
+
+
+def _encode_number(number):
+    return create_length_header(number, False)
+
+
+def _create_add_channel_response(channel_id, encoded_handshake,
+                                 encoding=0, rejected=False,
+                                 outer_frame_mask=False):
+    if encoding != 0 and encoding != 1:
+        raise ValueError('Invalid encoding %d' % encoding)
+
+    first_byte = ((_MUX_OPCODE_ADD_CHANNEL_RESPONSE << 5) |
+                  (rejected << 4) | encoding)
+    block = (chr(first_byte) +
+             _encode_channel_id(channel_id) +
+             _encode_number(len(encoded_handshake)) +
+             encoded_handshake)
+    payload = _encode_channel_id(_CONTROL_CHANNEL_ID) + block
+    return create_binary_frame(payload, mask=outer_frame_mask)
+
+
+def _create_drop_channel(channel_id, code=None, message='',
+                         outer_frame_mask=False):
+    if len(message) > 0 and code is None:
+        raise ValueError('Code must be specified if message is specified')
+
+    first_byte = _MUX_OPCODE_DROP_CHANNEL << 5
+    block = chr(first_byte) + _encode_channel_id(channel_id)
+    if code is None:
+        block += _encode_number(0) # Reason size
+    else:
+        reason = struct.pack('!H', code) + message
+        reason_size = _encode_number(len(reason))
+        block += reason_size + reason
+
+    payload = _encode_channel_id(_CONTROL_CHANNEL_ID) + block
+    return create_binary_frame(payload, mask=outer_frame_mask)
+
+
+def _create_flow_control(channel_id, replenished_quota,
+                         outer_frame_mask=False):
+    first_byte = _MUX_OPCODE_FLOW_CONTROL << 5
+    block = (chr(first_byte) +
+             _encode_channel_id(channel_id) +
+             _encode_number(replenished_quota))
+    payload = _encode_channel_id(_CONTROL_CHANNEL_ID) + block
+    return create_binary_frame(payload, mask=outer_frame_mask)
+
+
+def _create_new_channel_slot(slots, send_quota, outer_frame_mask=False):
+    if slots < 0 or send_quota < 0:
+        raise ValueError('slots and send_quota must be non-negative.')
+    first_byte = _MUX_OPCODE_NEW_CHANNEL_SLOT << 5
+    block = (chr(first_byte) +
+             _encode_number(slots) +
+             _encode_number(send_quota))
+    payload = _encode_channel_id(_CONTROL_CHANNEL_ID) + block
+    return create_binary_frame(payload, mask=outer_frame_mask)
+
+
+def _create_fallback_new_channel_slot(outer_frame_mask=False):
+    first_byte = (_MUX_OPCODE_NEW_CHANNEL_SLOT << 5) | 1 # Set the F flag
+    block = (chr(first_byte) + _encode_number(0) + _encode_number(0))
+    payload = _encode_channel_id(_CONTROL_CHANNEL_ID) + block
+    return create_binary_frame(payload, mask=outer_frame_mask)
+
+
+def _parse_request_text(request_text):
+    request_line, header_lines = request_text.split('\r\n', 1)
+
+    words = request_line.split(' ')
+    if len(words) != 3:
+        raise ValueError('Bad Request-Line syntax %r' % request_line)
+    [command, path, version] = words
+    if version != 'HTTP/1.1':
+        raise ValueError('Bad request version %r' % version)
+
+    # email.parser.Parser() parses RFC 2822 (RFC 822) style headers.
+    # RFC 6455 refers RFC 2616 for handshake parsing, and RFC 2616 refers
+    # RFC 822.
+    headers = email.parser.Parser().parsestr(header_lines)
+    return command, path, version, headers
+
+
+class _ControlBlock(object):
+    """A structure that holds parsing result of multiplexing control block.
+    Control block specific attributes will be added by _MuxFramePayloadParser.
+    (e.g. encoded_handshake will be added for AddChannelRequest and
+    AddChannelResponse)
+    """
+
+    def __init__(self, opcode):
+        self.opcode = opcode
+
+
+class _MuxFramePayloadParser(object):
+    """A class that parses multiplexed frame payload."""
+
+    def __init__(self, payload):
+        self._data = payload
+        self._read_position = 0
+        self._logger = util.get_class_logger(self)
+
+    def read_channel_id(self):
+        """Reads channel id.
+
+        Raises:
+            ValueError: when the payload doesn't contain
+                valid channel id.
+        """
+
+        remaining_length = len(self._data) - self._read_position
+        pos = self._read_position
+        if remaining_length == 0:
+            raise ValueError('Invalid channel id format')
+
+        channel_id = ord(self._data[pos])
+        channel_id_length = 1
+        if channel_id & 0xe0 == 0xe0:
+            if remaining_length < 4:
+                raise ValueError('Invalid channel id format')
+            channel_id = struct.unpack('!L',
+                                       self._data[pos:pos+4])[0] & 0x1fffffff
+            channel_id_length = 4
+        elif channel_id & 0xc0 == 0xc0:
+            if remaining_length < 3:
+                raise ValueError('Invalid channel id format')
+            channel_id = (((channel_id & 0x1f) << 16) +
+                          struct.unpack('!H', self._data[pos+1:pos+3])[0])
+            channel_id_length = 3
+        elif channel_id & 0x80 == 0x80:
+            if remaining_length < 2:
+                raise ValueError('Invalid channel id format')
+            channel_id = struct.unpack('!H',
+                                       self._data[pos:pos+2])[0] & 0x3fff
+            channel_id_length = 2
+        self._read_position += channel_id_length
+
+        return channel_id
+
+    def read_inner_frame(self):
+        """Reads an inner frame.
+
+        Raises:
+            PhysicalConnectionError: when the inner frame is invalid.
+        """
+
+        if len(self._data) == self._read_position:
+            raise PhysicalConnectionError(
+                _DROP_CODE_ENCAPSULATED_FRAME_IS_TRUNCATED)
+
+        bits = ord(self._data[self._read_position])
+        self._read_position += 1
+        fin = (bits & 0x80) == 0x80
+        rsv1 = (bits & 0x40) == 0x40
+        rsv2 = (bits & 0x20) == 0x20
+        rsv3 = (bits & 0x10) == 0x10
+        opcode = bits & 0xf
+        payload = self.remaining_data()
+        # Consume rest of the message which is payload data of the original
+        # frame.
+        self._read_position = len(self._data)
+        return fin, rsv1, rsv2, rsv3, opcode, payload
+
+    def _read_number(self):
+        if self._read_position + 1 > len(self._data):
+            raise PhysicalConnectionError(
+                _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+                'Cannot read the first byte of number field')
+
+        number = ord(self._data[self._read_position])
+        if number & 0x80 == 0x80:
+            raise PhysicalConnectionError(
+                _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+                'The most significant bit of the first byte of number should '
+                'be unset')
+        self._read_position += 1
+        pos = self._read_position
+        if number == 127:
+            if pos + 8 > len(self._data):
+                raise PhysicalConnectionError(
+                    _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+                    'Invalid number field')
+            self._read_position += 8
+            number = struct.unpack('!Q', self._data[pos:pos+8])[0]
+            if number > 0x7FFFFFFFFFFFFFFF:
+                raise PhysicalConnectionError(
+                    _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+                    'Encoded number >= 2^63')
+            if number <= 0xFFFF:
+                raise PhysicalConnectionError(
+                    _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+                    '%d should not be encoded by 9 bytes encoding' % number)
+            return number
+        if number == 126:
+            if pos + 2 > len(self._data):
+                raise PhysicalConnectionError(
+                    _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+                    'Invalid number field')
+            self._read_position += 2
+            number = struct.unpack('!H', self._data[pos:pos+2])[0]
+            if number <= 125:
+                raise PhysicalConnectionError(
+                    _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+                    '%d should not be encoded by 3 bytes encoding' % number)
+        return number
+
+    def _read_size_and_contents(self):
+        """Reads data that consists of followings:
+            - the size of the contents encoded the same way as payload length
+              of the WebSocket Protocol with 1 bit padding at the head.
+            - the contents.
+        """
+
+        size = self._read_number()
+        pos = self._read_position
+        if pos + size > len(self._data):
+            raise PhysicalConnectionError(
+                _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+                'Cannot read %d bytes data' % size)
+
+        self._read_position += size
+        return self._data[pos:pos+size]
+
+    def _read_add_channel_request(self, first_byte, control_block):
+        reserved = (first_byte >> 2) & 0x7
+        if reserved != 0:
+            raise PhysicalConnectionError(
+                _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+                'Reserved bits must be unset')
+
+        # Invalid encoding will be handled by MuxHandler.
+        encoding = first_byte & 0x3
+        try:
+            control_block.channel_id = self.read_channel_id()
+        except ValueError, e:
+            raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK)
+        control_block.encoding = encoding
+        encoded_handshake = self._read_size_and_contents()
+        control_block.encoded_handshake = encoded_handshake
+        return control_block
+
+    def _read_add_channel_response(self, first_byte, control_block):
+        reserved = (first_byte >> 2) & 0x3
+        if reserved != 0:
+            raise PhysicalConnectionError(
+                _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+                'Reserved bits must be unset')
+
+        control_block.accepted = (first_byte >> 4) & 1
+        control_block.encoding = first_byte & 0x3
+        try:
+            control_block.channel_id = self.read_channel_id()
+        except ValueError, e:
+            raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK)
+        control_block.encoded_handshake = self._read_size_and_contents()
+        return control_block
+
+    def _read_flow_control(self, first_byte, control_block):
+        reserved = first_byte & 0x1f
+        if reserved != 0:
+            raise PhysicalConnectionError(
+                _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+                'Reserved bits must be unset')
+
+        try:
+            control_block.channel_id = self.read_channel_id()
+        except ValueError, e:
+            raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK)
+        control_block.send_quota = self._read_number()
+        return control_block
+
+    def _read_drop_channel(self, first_byte, control_block):
+        reserved = first_byte & 0x1f
+        if reserved != 0:
+            raise PhysicalConnectionError(
+                _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+                'Reserved bits must be unset')
+
+        try:
+            control_block.channel_id = self.read_channel_id()
+        except ValueError, e:
+            raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK)
+        reason = self._read_size_and_contents()
+        if len(reason) == 0:
+            control_block.drop_code = None
+            control_block.drop_message = ''
+        elif len(reason) >= 2:
+            control_block.drop_code = struct.unpack('!H', reason[:2])[0]
+            control_block.drop_message = reason[2:]
+        else:
+            raise PhysicalConnectionError(
+                _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+                'Received DropChannel that conains only 1-byte reason')
+        return control_block
+
+    def _read_new_channel_slot(self, first_byte, control_block):
+        reserved = first_byte & 0x1e
+        if reserved != 0:
+            raise PhysicalConnectionError(
+                _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+                'Reserved bits must be unset')
+        control_block.fallback = first_byte & 1
+        control_block.slots = self._read_number()
+        control_block.send_quota = self._read_number()
+        return control_block
+
+    def read_control_blocks(self):
+        """Reads control block(s).
+
+        Raises:
+           PhysicalConnectionError: when the payload contains invalid control
+               block(s).
+           StopIteration: when no control blocks left.
+        """
+
+        while self._read_position < len(self._data):
+            first_byte = ord(self._data[self._read_position])
+            self._read_position += 1
+            opcode = (first_byte >> 5) & 0x7
+            control_block = _ControlBlock(opcode=opcode)
+            if opcode == _MUX_OPCODE_ADD_CHANNEL_REQUEST:
+                yield self._read_add_channel_request(first_byte, control_block)
+            elif opcode == _MUX_OPCODE_ADD_CHANNEL_RESPONSE:
+                yield self._read_add_channel_response(
+                    first_byte, control_block)
+            elif opcode == _MUX_OPCODE_FLOW_CONTROL:
+                yield self._read_flow_control(first_byte, control_block)
+            elif opcode == _MUX_OPCODE_DROP_CHANNEL:
+                yield self._read_drop_channel(first_byte, control_block)
+            elif opcode == _MUX_OPCODE_NEW_CHANNEL_SLOT:
+                yield self._read_new_channel_slot(first_byte, control_block)
+            else:
+                raise PhysicalConnectionError(
+                    _DROP_CODE_UNKNOWN_MUX_OPCODE,
+                    'Invalid opcode %d' % opcode)
+
+        assert self._read_position == len(self._data)
+        raise StopIteration
+
+    def remaining_data(self):
+        """Returns remaining data."""
+
+        return self._data[self._read_position:]
+
+
+class _LogicalRequest(object):
+    """Mimics mod_python request."""
+
+    def __init__(self, channel_id, command, path, protocol, headers,
+                 connection):
+        """Constructs an instance.
+
+        Args:
+            channel_id: the channel id of the logical channel.
+            command: HTTP request command.
+            path: HTTP request path.
+            headers: HTTP headers.
+            connection: _LogicalConnection instance.
+        """
+
+        self.channel_id = channel_id
+        self.method = command
+        self.uri = path
+        self.protocol = protocol
+        self.headers_in = headers
+        self.connection = connection
+        self.server_terminated = False
+        self.client_terminated = False
+
+    def is_https(self):
+        """Mimics request.is_https(). Returns False because this method is
+        used only by old protocols (hixie and hybi00).
+        """
+
+        return False
+
+
+class _LogicalConnection(object):
+    """Mimics mod_python mp_conn."""
+
+    # For details, see the comment of set_read_state().
+    STATE_ACTIVE = 1
+    STATE_GRACEFULLY_CLOSED = 2
+    STATE_TERMINATED = 3
+
+    def __init__(self, mux_handler, channel_id):
+        """Constructs an instance.
+
+        Args:
+            mux_handler: _MuxHandler instance.
+            channel_id: channel id of this connection.
+        """
+
+        self._mux_handler = mux_handler
+        self._channel_id = channel_id
+        self._incoming_data = ''
+        self._write_condition = threading.Condition()
+        self._waiting_write_completion = False
+        self._read_condition = threading.Condition()
+        self._read_state = self.STATE_ACTIVE
+
+    def get_local_addr(self):
+        """Getter to mimic mp_conn.local_addr."""
+
+        return self._mux_handler.physical_connection.get_local_addr()
+    local_addr = property(get_local_addr)
+
+    def get_remote_addr(self):
+        """Getter to mimic mp_conn.remote_addr."""
+
+        return self._mux_handler.physical_connection.get_remote_addr()
+    remote_addr = property(get_remote_addr)
+
+    def get_memorized_lines(self):
+        """Gets memorized lines. Not supported."""
+
+        raise MuxUnexpectedException('_LogicalConnection does not support '
+                                     'get_memorized_lines')
+
+    def write(self, data):
+        """Writes data. mux_handler sends data asynchronously. The caller will
+        be suspended until write done.
+
+        Args:
+            data: data to be written.
+
+        Raises:
+            MuxUnexpectedException: when called before finishing the previous
+                write.
+        """
+
+        try:
+            self._write_condition.acquire()
+            if self._waiting_write_completion:
+                raise MuxUnexpectedException(
+                    'Logical connection %d is already waiting the completion '
+                    'of write' % self._channel_id)
+
+            self._waiting_write_completion = True
+            self._mux_handler.send_data(self._channel_id, data)
+            self._write_condition.wait()
+        finally:
+            self._write_condition.release()
+
+    def write_control_data(self, data):
+        """Writes data via the control channel. Don't wait finishing write
+        because this method can be called by mux dispatcher.
+
+        Args:
+            data: data to be written.
+        """
+
+        self._mux_handler.send_control_data(data)
+
+    def notify_write_done(self):
+        """Called when sending data is completed."""
+
+        try:
+            self._write_condition.acquire()
+            if not self._waiting_write_completion:
+                raise MuxUnexpectedException(
+                    'Invalid call of notify_write_done for logical connection'
+                    ' %d' % self._channel_id)
+            self._waiting_write_completion = False
+            self._write_condition.notify()
+        finally:
+            self._write_condition.release()
+
+    def append_frame_data(self, frame_data):
+        """Appends incoming frame data. Called when mux_handler dispatches
+        frame data to the corresponding application.
+
+        Args:
+            frame_data: incoming frame data.
+        """
+
+        self._read_condition.acquire()
+        self._incoming_data += frame_data
+        self._read_condition.notify()
+        self._read_condition.release()
+
+    def read(self, length):
+        """Reads data. Blocks until enough data has arrived via physical
+        connection.
+
+        Args:
+            length: length of data to be read.
+        Raises:
+            LogicalConnectionClosedException: when closing handshake for this
+                logical channel has been received.
+            ConnectionTerminatedException: when the physical connection has
+                closed, or an error is caused on the reader thread.
+        """
+
+        self._read_condition.acquire()
+        while (self._read_state == self.STATE_ACTIVE and
+               len(self._incoming_data) < length):
+            self._read_condition.wait()
+
+        try:
+            if self._read_state == self.STATE_GRACEFULLY_CLOSED:
+                raise LogicalConnectionClosedException(
+                    'Logical channel %d has closed.' % self._channel_id)
+            elif self._read_state == self.STATE_TERMINATED:
+                raise ConnectionTerminatedException(
+                    'Receiving %d byte failed. Logical channel (%d) closed' %
+                    (length, self._channel_id))
+
+            value = self._incoming_data[:length]
+            self._incoming_data = self._incoming_data[length:]
+        finally:
+            self._read_condition.release()
+
+        return value
+
+    def set_read_state(self, new_state):
+        """Sets the state of this connection. Called when an event for this
+        connection has occurred.
+
+        Args:
+            new_state: state to be set. new_state must be one of followings:
+            - STATE_GRACEFULLY_CLOSED: when closing handshake for this
+                connection has been received.
+            - STATE_TERMINATED: when the physical connection has closed or
+                DropChannel of this connection has received.
+        """
+
+        self._read_condition.acquire()
+        self._read_state = new_state
+        self._read_condition.notify()
+        self._read_condition.release()
+
+
+class _LogicalStream(Stream):
+    """Mimics the Stream class. This class interprets multiplexed WebSocket
+    frames.
+    """
+
+    def __init__(self, request, send_quota, receive_quota):
+        """Constructs an instance.
+
+        Args:
+            request: _LogicalRequest instance.
+            send_quota: Initial send quota.
+            receive_quota: Initial receive quota.
+        """
+
+        # TODO(bashi): Support frame filters.
+        stream_options = StreamOptions()
+        # Physical stream is responsible for masking.
+        stream_options.unmask_receive = False
+        # Control frames can be fragmented on logical channel.
+        stream_options.allow_fragmented_control_frame = True
+        Stream.__init__(self, request, stream_options)
+        self._send_quota = send_quota
+        self._send_quota_condition = threading.Condition()
+        self._receive_quota = receive_quota
+        self._write_inner_frame_semaphore = threading.Semaphore()
+
+    def _create_inner_frame(self, opcode, payload, end=True):
+        # TODO(bashi): Support extensions that use reserved bits.
+        first_byte = (end << 7) | opcode
+        return (_encode_channel_id(self._request.channel_id) +
+                chr(first_byte) + payload)
+
+    def _write_inner_frame(self, opcode, payload, end=True):
+        payload_length = len(payload)
+        write_position = 0
+
+        try:
+            # An inner frame will be fragmented if there is no enough send
+            # quota. This semaphore ensures that fragmented inner frames are
+            # sent in order on the logical channel.
+            # Note that frames that come from other logical channels or
+            # multiplexing control blocks can be inserted between fragmented
+            # inner frames on the physical channel.
+            self._write_inner_frame_semaphore.acquire()
+            while write_position < payload_length:
+                try:
+                    self._send_quota_condition.acquire()
+                    while self._send_quota == 0:
+                        self._logger.debug(
+                            'No quota. Waiting FlowControl message for %d.' %
+                            self._request.channel_id)
+                        self._send_quota_condition.wait()
+
+                    remaining = payload_length - write_position
+                    write_length = min(self._send_quota, remaining)
+                    inner_frame_end = (
+                        end and
+                        (write_position + write_length == payload_length))
+
+                    inner_frame = self._create_inner_frame(
+                        opcode,
+                        payload[write_position:write_position+write_length],
+                        inner_frame_end)
+                    frame_data = self._writer.build(
+                        inner_frame, end=True, binary=True)
+                    self._send_quota -= write_length
+                    self._logger.debug('Consumed quota=%d, remaining=%d' %
+                                       (write_length, self._send_quota))
+                finally:
+                    self._send_quota_condition.release()
+
+                # Writing data will block the worker so we need to release
+                # _send_quota_condition before writing.
+                self._logger.debug('Sending inner frame: %r' % frame_data)
+                self._request.connection.write(frame_data)
+                write_position += write_length
+
+                opcode = common.OPCODE_CONTINUATION
+
+        except ValueError, e:
+            raise BadOperationException(e)
+        finally:
+            self._write_inner_frame_semaphore.release()
+
+    def replenish_send_quota(self, send_quota):
+        """Replenish send quota."""
+
+        self._send_quota_condition.acquire()
+        self._send_quota += send_quota
+        self._logger.debug('Replenished send quota for channel id %d: %d' %
+                           (self._request.channel_id, self._send_quota))
+        self._send_quota_condition.notify()
+        self._send_quota_condition.release()
+
+    def consume_receive_quota(self, amount):
+        """Consumes receive quota. Returns False on failure."""
+
+        if self._receive_quota < amount:
+            self._logger.debug('Violate quota on channel id %d: %d < %d' %
+                               (self._request.channel_id,
+                                self._receive_quota, amount))
+            return False
+        self._receive_quota -= amount
+        return True
+
+    def send_message(self, message, end=True, binary=False):
+        """Override Stream.send_message."""
+
+        if self._request.server_terminated:
+            raise BadOperationException(
+                'Requested send_message after sending out a closing handshake')
+
+        if binary and isinstance(message, unicode):
+            raise BadOperationException(
+                'Message for binary frame must be instance of str')
+
+        if binary:
+            opcode = common.OPCODE_BINARY
+        else:
+            opcode = common.OPCODE_TEXT
+            message = message.encode('utf-8')
+
+        self._write_inner_frame(opcode, message, end)
+
+    def _receive_frame(self):
+        """Overrides Stream._receive_frame.
+
+        In addition to call Stream._receive_frame, this method adds the amount
+        of payload to receiving quota and sends FlowControl to the client.
+        We need to do it here because Stream.receive_message() handles
+        control frames internally.
+        """
+
+        opcode, payload, fin, rsv1, rsv2, rsv3 = Stream._receive_frame(self)
+        amount = len(payload)
+        self._receive_quota += amount
+        frame_data = _create_flow_control(self._request.channel_id,
+                                          amount)
+        self._logger.debug('Sending flow control for %d, replenished=%d' %
+                           (self._request.channel_id, amount))
+        self._request.connection.write_control_data(frame_data)
+        return opcode, payload, fin, rsv1, rsv2, rsv3
+
+    def receive_message(self):
+        """Overrides Stream.receive_message."""
+
+        # Just call Stream.receive_message(), but catch
+        # LogicalConnectionClosedException, which is raised when the logical
+        # connection has closed gracefully.
+        try:
+            return Stream.receive_message(self)
+        except LogicalConnectionClosedException, e:
+            self._logger.debug('%s', e)
+            return None
+
+    def _send_closing_handshake(self, code, reason):
+        """Overrides Stream._send_closing_handshake."""
+
+        body = create_closing_handshake_body(code, reason)
+        self._logger.debug('Sending closing handshake for %d: (%r, %r)' %
+                           (self._request.channel_id, code, reason))
+        self._write_inner_frame(common.OPCODE_CLOSE, body, end=True)
+
+        self._request.server_terminated = True
+
+    def send_ping(self, body=''):
+        """Overrides Stream.send_ping"""
+
+        self._logger.debug('Sending ping on logical channel %d: %r' %
+                           (self._request.channel_id, body))
+        self._write_inner_frame(common.OPCODE_PING, body, end=True)
+
+        self._ping_queue.append(body)
+
+    def _send_pong(self, body):
+        """Overrides Stream._send_pong"""
+
+        self._logger.debug('Sending pong on logical channel %d: %r' %
+                           (self._request.channel_id, body))
+        self._write_inner_frame(common.OPCODE_PONG, body, end=True)
+
+    def close_connection(self, code=common.STATUS_NORMAL_CLOSURE, reason=''):
+        """Overrides Stream.close_connection."""
+
+        # TODO(bashi): Implement
+        self._logger.debug('Closing logical connection %d' %
+                           self._request.channel_id)
+        self._request.server_terminated = True
+
+    def _drain_received_data(self):
+        """Overrides Stream._drain_received_data. Nothing need to be done for
+        logical channel.
+        """
+
+        pass
+
+
+class _OutgoingData(object):
+    """A structure that holds data to be sent via physical connection and
+    origin of the data.
+    """
+
+    def __init__(self, channel_id, data):
+        self.channel_id = channel_id
+        self.data = data
+
+
+class _PhysicalConnectionWriter(threading.Thread):
+    """A thread that is responsible for writing data to physical connection.
+
+    TODO(bashi): Make sure there is no thread-safety problem when the reader
+    thread reads data from the same socket at a time.
+    """
+
+    def __init__(self, mux_handler):
+        """Constructs an instance.
+
+        Args:
+            mux_handler: _MuxHandler instance.
+        """
+
+        threading.Thread.__init__(self)
+        self._logger = util.get_class_logger(self)
+        self._mux_handler = mux_handler
+        self.setDaemon(True)
+        self._stop_requested = False
+        self._deque = collections.deque()
+        self._deque_condition = threading.Condition()
+
+    def put_outgoing_data(self, data):
+        """Puts outgoing data.
+
+        Args:
+            data: _OutgoingData instance.
+
+        Raises:
+            BadOperationException: when the thread has been requested to
+                terminate.
+        """
+
+        try:
+            self._deque_condition.acquire()
+            if self._stop_requested:
+                raise BadOperationException('Cannot write data anymore')
+
+            self._deque.append(data)
+            self._deque_condition.notify()
+        finally:
+            self._deque_condition.release()
+
+    def _write_data(self, outgoing_data):
+        try:
+            self._mux_handler.physical_connection.write(outgoing_data.data)
+        except Exception, e:
+            util.prepend_message_to_exception(
+                'Failed to send message to %r: ' %
+                (self._mux_handler.physical_connection.remote_addr,), e)
+            raise
+
+        # TODO(bashi): It would be better to block the thread that sends
+        # control data as well.
+        if outgoing_data.channel_id != _CONTROL_CHANNEL_ID:
+            self._mux_handler.notify_write_done(outgoing_data.channel_id)
+
+    def run(self):
+        self._deque_condition.acquire()
+        while not self._stop_requested:
+            if len(self._deque) == 0:
+                self._deque_condition.wait()
+                continue
+
+            outgoing_data = self._deque.popleft()
+            self._deque_condition.release()
+            self._write_data(outgoing_data)
+            self._deque_condition.acquire()
+
+        # Flush deque
+        try:
+            while len(self._deque) > 0:
+                outgoing_data = self._deque.popleft()
+                self._write_data(outgoing_data)
+        finally:
+            self._deque_condition.release()
+
+    def stop(self):
+        """Stops the writer thread."""
+
+        self._deque_condition.acquire()
+        self._stop_requested = True
+        self._deque_condition.notify()
+        self._deque_condition.release()
+
+
+class _PhysicalConnectionReader(threading.Thread):
+    """A thread that is responsible for reading data from physical connection.
+    """
+
+    def __init__(self, mux_handler):
+        """Constructs an instance.
+
+        Args:
+            mux_handler: _MuxHandler instance.
+        """
+
+        threading.Thread.__init__(self)
+        self._logger = util.get_class_logger(self)
+        self._mux_handler = mux_handler
+        self.setDaemon(True)
+
+    def run(self):
+        while True:
+            try:
+                physical_stream = self._mux_handler.physical_stream
+                message = physical_stream.receive_message()
+                if message is None:
+                    break
+                # Below happens only when a data message is received.
+                opcode = physical_stream.get_last_received_opcode()
+                if opcode != common.OPCODE_BINARY:
+                    self._mux_handler.fail_physical_connection(
+                        _DROP_CODE_INVALID_ENCAPSULATING_MESSAGE,
+                        'Received a text message on physical connection')
+                    break
+
+            except ConnectionTerminatedException, e:
+                self._logger.debug('%s', e)
+                break
+
+            try:
+                self._mux_handler.dispatch_message(message)
+            except PhysicalConnectionError, e:
+                self._mux_handler.fail_physical_connection(
+                    e.drop_code, e.message)
+                break
+            except LogicalChannelError, e:
+                self._mux_handler.fail_logical_channel(
+                    e.channel_id, e.drop_code, e.message)
+            except Exception, e:
+                self._logger.debug(traceback.format_exc())
+                break
+
+        self._mux_handler.notify_reader_done()
+
+
+class _Worker(threading.Thread):
+    """A thread that is responsible for running the corresponding application
+    handler.
+    """
+
+    def __init__(self, mux_handler, request):
+        """Constructs an instance.
+
+        Args:
+            mux_handler: _MuxHandler instance.
+            request: _LogicalRequest instance.
+        """
+
+        threading.Thread.__init__(self)
+        self._logger = util.get_class_logger(self)
+        self._mux_handler = mux_handler
+        self._request = request
+        self.setDaemon(True)
+
+    def run(self):
+        self._logger.debug('Logical channel worker started. (id=%d)' %
+                           self._request.channel_id)
+        try:
+            # Non-critical exceptions will be handled by dispatcher.
+            self._mux_handler.dispatcher.transfer_data(self._request)
+        finally:
+            self._mux_handler.notify_worker_done(self._request.channel_id)
+
+
+class _MuxHandshaker(hybi.Handshaker):
+    """Opening handshake processor for multiplexing."""
+
+    _DUMMY_WEBSOCKET_KEY = 'dGhlIHNhbXBsZSBub25jZQ=='
+
+    def __init__(self, request, dispatcher, send_quota, receive_quota):
+        """Constructs an instance.
+        Args:
+            request: _LogicalRequest instance.
+            dispatcher: Dispatcher instance (dispatch.Dispatcher).
+            send_quota: Initial send quota.
+            receive_quota: Initial receive quota.
+        """
+
+        hybi.Handshaker.__init__(self, request, dispatcher)
+        self._send_quota = send_quota
+        self._receive_quota = receive_quota
+
+        # Append headers which should not be included in handshake field of
+        # AddChannelRequest.
+        # TODO(bashi): Make sure whether we should raise exception when
+        #     these headers are included already.
+        request.headers_in[common.UPGRADE_HEADER] = (
+            common.WEBSOCKET_UPGRADE_TYPE)
+        request.headers_in[common.CONNECTION_HEADER] = (
+            common.UPGRADE_CONNECTION_TYPE)
+        request.headers_in[common.SEC_WEBSOCKET_VERSION_HEADER] = (
+            str(common.VERSION_HYBI_LATEST))
+        request.headers_in[common.SEC_WEBSOCKET_KEY_HEADER] = (
+            self._DUMMY_WEBSOCKET_KEY)
+
+    def _create_stream(self, stream_options):
+        """Override hybi.Handshaker._create_stream."""
+
+        self._logger.debug('Creating logical stream for %d' %
+                           self._request.channel_id)
+        return _LogicalStream(self._request, self._send_quota,
+                              self._receive_quota)
+
+    def _create_handshake_response(self, accept):
+        """Override hybi._create_handshake_response."""
+
+        response = []
+
+        response.append('HTTP/1.1 101 Switching Protocols\r\n')
+
+        # Upgrade, Connection and Sec-WebSocket-Accept should be excluded.
+        if self._request.ws_protocol is not None:
+            response.append('%s: %s\r\n' % (
+                common.SEC_WEBSOCKET_PROTOCOL_HEADER,
+                self._request.ws_protocol))
+        if (self._request.ws_extensions is not None and
+            len(self._request.ws_extensions) != 0):
+            response.append('%s: %s\r\n' % (
+                common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
+                common.format_extensions(self._request.ws_extensions)))
+        response.append('\r\n')
+
+        return ''.join(response)
+
+    def _send_handshake(self, accept):
+        """Override hybi.Handshaker._send_handshake."""
+
+        # Don't send handshake response for the default channel
+        if self._request.channel_id == _DEFAULT_CHANNEL_ID:
+            return
+
+        handshake_response = self._create_handshake_response(accept)
+        frame_data = _create_add_channel_response(
+                         self._request.channel_id,
+                         handshake_response)
+        self._logger.debug('Sending handshake response for %d: %r' %
+                           (self._request.channel_id, frame_data))
+        self._request.connection.write_control_data(frame_data)
+
+
+class _LogicalChannelData(object):
+    """A structure that holds information about logical channel.
+    """
+
+    def __init__(self, request, worker):
+        self.request = request
+        self.worker = worker
+        self.drop_code = _DROP_CODE_NORMAL_CLOSURE
+        self.drop_message = ''
+
+
+class _HandshakeDeltaBase(object):
+    """A class that holds information for delta-encoded handshake."""
+
+    def __init__(self, headers):
+        self._headers = headers
+
+    def create_headers(self, delta=None):
+        """Creates request headers for an AddChannelRequest that has
+        delta-encoded handshake.
+
+        Args:
+            delta: headers should be overridden.
+        """
+
+        headers = copy.copy(self._headers)
+        if delta:
+            for key, value in delta.items():
+                # The spec requires that a header with an empty value is
+                # removed from the delta base.
+                if len(value) == 0 and headers.has_key(key):
+                    del headers[key]
+                else:
+                    headers[key] = value
+        # TODO(bashi): Support extensions
+        headers['Sec-WebSocket-Extensions'] = ''
+        return headers
+
+
+class _MuxHandler(object):
+    """Multiplexing handler. When a handler starts, it launches three
+    threads; the reader thread, the writer thread, and a worker thread.
+
+    The reader thread reads data from the physical stream, i.e., the
+    ws_stream object of the underlying websocket connection. The reader
+    thread interprets multiplexed frames and dispatches them to logical
+    channels. Methods of this class are mostly called by the reader thread.
+
+    The writer thread sends multiplexed frames which are created by
+    logical channels via the physical connection.
+
+    The worker thread launched at the starting point handles the
+    "Implicitly Opened Connection". If multiplexing handler receives
+    an AddChannelRequest and accepts it, the handler will launch a new worker
+    thread and dispatch the request to it.
+    """
+
+    def __init__(self, request, dispatcher):
+        """Constructs an instance.
+
+        Args:
+            request: mod_python request of the physical connection.
+            dispatcher: Dispatcher instance (dispatch.Dispatcher).
+        """
+
+        self.original_request = request
+        self.dispatcher = dispatcher
+        self.physical_connection = request.connection
+        self.physical_stream = request.ws_stream
+        self._logger = util.get_class_logger(self)
+        self._logical_channels = {}
+        self._logical_channels_condition = threading.Condition()
+        # Holds client's initial quota
+        self._channel_slots = collections.deque()
+        self._handshake_base = None
+        self._worker_done_notify_received = False
+        self._reader = None
+        self._writer = None
+
+    def start(self):
+        """Starts the handler.
+
+        Raises:
+            MuxUnexpectedException: when the handler already started, or when
+                opening handshake of the default channel fails.
+        """
+
+        if self._reader or self._writer:
+            raise MuxUnexpectedException('MuxHandler already started')
+
+        self._reader = _PhysicalConnectionReader(self)
+        self._writer = _PhysicalConnectionWriter(self)
+        self._reader.start()
+        self._writer.start()
+
+        # Create "Implicitly Opened Connection".
+        logical_connection = _LogicalConnection(self, _DEFAULT_CHANNEL_ID)
+        self._handshake_base = _HandshakeDeltaBase(
+            self.original_request.headers_in)
+        logical_request = _LogicalRequest(
+            _DEFAULT_CHANNEL_ID,
+            self.original_request.method,
+            self.original_request.uri,
+            self.original_request.protocol,
+            self._handshake_base.create_headers(),
+            logical_connection)
+        # Client's send quota for the implicitly opened connection is zero,
+        # but we will send FlowControl later so set the initial quota to
+        # _INITIAL_QUOTA_FOR_CLIENT.
+        self._channel_slots.append(_INITIAL_QUOTA_FOR_CLIENT)
+        if not self._do_handshake_for_logical_request(
+            logical_request, send_quota=self.original_request.mux_quota):
+            raise MuxUnexpectedException(
+                'Failed handshake on the default channel id')
+        self._add_logical_channel(logical_request)
+
+        # Send FlowControl for the implicitly opened connection.
+        frame_data = _create_flow_control(_DEFAULT_CHANNEL_ID,
+                                          _INITIAL_QUOTA_FOR_CLIENT)
+        logical_request.connection.write_control_data(frame_data)
+
+    def add_channel_slots(self, slots, send_quota):
+        """Adds channel slots.
+
+        Args:
+            slots: number of slots to be added.
+            send_quota: initial send quota for slots.
+        """
+
+        self._channel_slots.extend([send_quota] * slots)
+        # Send NewChannelSlot to client.
+        frame_data = _create_new_channel_slot(slots, send_quota)
+        self.send_control_data(frame_data)
+
+    def wait_until_done(self, timeout=None):
+        """Waits until all workers are done. Returns False when timeout has
+        occurred. Returns True on success.
+
+        Args:
+            timeout: timeout in sec.
+        """
+
+        self._logical_channels_condition.acquire()
+        try:
+            while len(self._logical_channels) > 0:
+                self._logger.debug('Waiting workers(%d)...' %
+                                   len(self._logical_channels))
+                self._worker_done_notify_received = False
+                self._logical_channels_condition.wait(timeout)
+                if not self._worker_done_notify_received:
+                    self._logger.debug('Waiting worker(s) timed out')
+                    return False
+
+        finally:
+            self._logical_channels_condition.release()
+
+        # Flush pending outgoing data
+        self._writer.stop()
+        self._writer.join()
+
+        return True
+
+    def notify_write_done(self, channel_id):
+        """Called by the writer thread when a write operation has done.
+
+        Args:
+            channel_id: objective channel id.
+        """
+
+        try:
+            self._logical_channels_condition.acquire()
+            if channel_id in self._logical_channels:
+                channel_data = self._logical_channels[channel_id]
+                channel_data.request.connection.notify_write_done()
+            else:
+                self._logger.debug('Seems that logical channel for %d has gone'
+                                   % channel_id)
+        finally:
+            self._logical_channels_condition.release()
+
+    def send_control_data(self, data):
+        """Sends data via the control channel.
+
+        Args:
+            data: data to be sent.
+        """
+
+        self._writer.put_outgoing_data(_OutgoingData(
+                channel_id=_CONTROL_CHANNEL_ID, data=data))
+
+    def send_data(self, channel_id, data):
+        """Sends data via given logical channel. This method is called by
+        worker threads.
+
+        Args:
+            channel_id: objective channel id.
+            data: data to be sent.
+        """
+
+        self._writer.put_outgoing_data(_OutgoingData(
+                channel_id=channel_id, data=data))
+
+    def _send_drop_channel(self, channel_id, code=None, message=''):
+        frame_data = _create_drop_channel(channel_id, code, message)
+        self._logger.debug(
+            'Sending drop channel for channel id %d' % channel_id)
+        self.send_control_data(frame_data)
+
+    def _send_error_add_channel_response(self, channel_id, status=None):
+        if status is None:
+            status = common.HTTP_STATUS_BAD_REQUEST
+
+        if status in _HTTP_BAD_RESPONSE_MESSAGES:
+            message = _HTTP_BAD_RESPONSE_MESSAGES[status]
+        else:
+            self._logger.debug('Response message for %d is not found' % status)
+            message = '???'
+
+        response = 'HTTP/1.1 %d %s\r\n\r\n' % (status, message)
+        frame_data = _create_add_channel_response(channel_id,
+                                                  encoded_handshake=response,
+                                                  encoding=0, rejected=True)
+        self.send_control_data(frame_data)
+
+    def _create_logical_request(self, block):
+        if block.channel_id == _CONTROL_CHANNEL_ID:
+            # TODO(bashi): Raise PhysicalConnectionError with code 2006
+            # instead of MuxUnexpectedException.
+            raise MuxUnexpectedException(
+                'Received the control channel id (0) as objective channel '
+                'id for AddChannel')
+
+        if block.encoding > _HANDSHAKE_ENCODING_DELTA:
+            raise PhysicalConnectionError(
+                _DROP_CODE_UNKNOWN_REQUEST_ENCODING)
+
+        method, path, version, headers = _parse_request_text(
+            block.encoded_handshake)
+        if block.encoding == _HANDSHAKE_ENCODING_DELTA:
+            headers = self._handshake_base.create_headers(headers)
+
+        connection = _LogicalConnection(self, block.channel_id)
+        request = _LogicalRequest(block.channel_id, method, path, version,
+                                  headers, connection)
+        return request
+
+    def _do_handshake_for_logical_request(self, request, send_quota=0):
+        try:
+            receive_quota = self._channel_slots.popleft()
+        except IndexError:
+            raise LogicalChannelError(
+                request.channel_id, _DROP_CODE_NEW_CHANNEL_SLOT_VIOLATION)
+
+        handshaker = _MuxHandshaker(request, self.dispatcher,
+                                    send_quota, receive_quota)
+        try:
+            handshaker.do_handshake()
+        except handshake.VersionException, e:
+            self._logger.info('%s', e)
+            self._send_error_add_channel_response(
+                request.channel_id, status=common.HTTP_STATUS_BAD_REQUEST)
+            return False
+        except handshake.HandshakeException, e:
+            # TODO(bashi): Should we _Fail the Logical Channel_ with 3001
+            # instead?
+            self._logger.info('%s', e)
+            self._send_error_add_channel_response(request.channel_id,
+                                                  status=e.status)
+            return False
+        except handshake.AbortedByUserException, e:
+            self._logger.info('%s', e)
+            self._send_error_add_channel_response(request.channel_id)
+            return False
+
+        return True
+
+    def _add_logical_channel(self, logical_request):
+        try:
+            self._logical_channels_condition.acquire()
+            if logical_request.channel_id in self._logical_channels:
+                self._logger.debug('Channel id %d already exists' %
+                                   logical_request.channel_id)
+                raise PhysicalConnectionError(
+                    _DROP_CODE_CHANNEL_ALREADY_EXISTS,
+                    'Channel id %d already exists' %
+                    logical_request.channel_id)
+            worker = _Worker(self, logical_request)
+            channel_data = _LogicalChannelData(logical_request, worker)
+            self._logical_channels[logical_request.channel_id] = channel_data
+            worker.start()
+        finally:
+            self._logical_channels_condition.release()
+
+    def _process_add_channel_request(self, block):
+        try:
+            logical_request = self._create_logical_request(block)
+        except ValueError, e:
+            self._logger.debug('Failed to create logical request: %r' % e)
+            self._send_error_add_channel_response(
+                block.channel_id, status=common.HTTP_STATUS_BAD_REQUEST)
+            return
+        if self._do_handshake_for_logical_request(logical_request):
+            if block.encoding == _HANDSHAKE_ENCODING_IDENTITY:
+                # Update handshake base.
+                # TODO(bashi): Make sure this is the right place to update
+                # handshake base.
+                self._handshake_base = _HandshakeDeltaBase(
+                    logical_request.headers_in)
+            self._add_logical_channel(logical_request)
+        else:
+            self._send_error_add_channel_response(
+                block.channel_id, status=common.HTTP_STATUS_BAD_REQUEST)
+
+    def _process_flow_control(self, block):
+        try:
+            self._logical_channels_condition.acquire()
+            if not block.channel_id in self._logical_channels:
+                return
+            channel_data = self._logical_channels[block.channel_id]
+            channel_data.request.ws_stream.replenish_send_quota(
+                block.send_quota)
+        finally:
+            self._logical_channels_condition.release()
+
+    def _process_drop_channel(self, block):
+        self._logger.debug(
+            'DropChannel received for %d: code=%r, reason=%r' %
+            (block.channel_id, block.drop_code, block.drop_message))
+        try:
+            self._logical_channels_condition.acquire()
+            if not block.channel_id in self._logical_channels:
+                return
+            channel_data = self._logical_channels[block.channel_id]
+            channel_data.drop_code = _DROP_CODE_ACKNOWLEDGED
+            # Close the logical channel
+            channel_data.request.connection.set_read_state(
+                _LogicalConnection.STATE_TERMINATED)
+        finally:
+            self._logical_channels_condition.release()
+
+    def _process_control_blocks(self, parser):
+        for control_block in parser.read_control_blocks():
+            opcode = control_block.opcode
+            self._logger.debug('control block received, opcode: %d' % opcode)
+            if opcode == _MUX_OPCODE_ADD_CHANNEL_REQUEST:
+                self._process_add_channel_request(control_block)
+            elif opcode == _MUX_OPCODE_ADD_CHANNEL_RESPONSE:
+                raise PhysicalConnectionError(
+                    _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+                    'Received AddChannelResponse')
+            elif opcode == _MUX_OPCODE_FLOW_CONTROL:
+                self._process_flow_control(control_block)
+            elif opcode == _MUX_OPCODE_DROP_CHANNEL:
+                self._process_drop_channel(control_block)
+            elif opcode == _MUX_OPCODE_NEW_CHANNEL_SLOT:
+                raise PhysicalConnectionError(
+                    _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+                    'Received NewChannelSlot')
+            else:
+                raise MuxUnexpectedException(
+                    'Unexpected opcode %r' % opcode)
+
+    def _process_logical_frame(self, channel_id, parser):
+        self._logger.debug('Received a frame. channel id=%d' % channel_id)
+        try:
+            self._logical_channels_condition.acquire()
+            if not channel_id in self._logical_channels:
+                # We must ignore the message for an inactive channel.
+                return
+            channel_data = self._logical_channels[channel_id]
+            fin, rsv1, rsv2, rsv3, opcode, payload = parser.read_inner_frame()
+            if not channel_data.request.ws_stream.consume_receive_quota(
+                len(payload)):
+                # The client violates quota. Close logical channel.
+                raise LogicalChannelError(
+                    channel_id, _DROP_CODE_SEND_QUOTA_VIOLATION)
+            header = create_header(opcode, len(payload), fin, rsv1, rsv2, rsv3,
+                                   mask=False)
+            frame_data = header + payload
+            channel_data.request.connection.append_frame_data(frame_data)
+        finally:
+            self._logical_channels_condition.release()
+
+    def dispatch_message(self, message):
+        """Dispatches message. The reader thread calls this method.
+
+        Args:
+            message: a message that contains encapsulated frame.
+        Raises:
+            PhysicalConnectionError: if the message contains physical
+                connection level errors.
+            LogicalChannelError: if the message contains logical channel
+                level errors.
+        """
+
+        parser = _MuxFramePayloadParser(message)
+        try:
+            channel_id = parser.read_channel_id()
+        except ValueError, e:
+            raise PhysicalConnectionError(_DROP_CODE_CHANNEL_ID_TRUNCATED)
+        if channel_id == _CONTROL_CHANNEL_ID:
+            self._process_control_blocks(parser)
+        else:
+            self._process_logical_frame(channel_id, parser)
+
+    def notify_worker_done(self, channel_id):
+        """Called when a worker has finished.
+
+        Args:
+            channel_id: channel id corresponded with the worker.
+        """
+
+        self._logger.debug('Worker for channel id %d terminated' % channel_id)
+        try:
+            self._logical_channels_condition.acquire()
+            if not channel_id in self._logical_channels:
+                raise MuxUnexpectedException(
+                    'Channel id %d not found' % channel_id)
+            channel_data = self._logical_channels.pop(channel_id)
+        finally:
+            self._worker_done_notify_received = True
+            self._logical_channels_condition.notify()
+            self._logical_channels_condition.release()
+
+        if not channel_data.request.server_terminated:
+            self._send_drop_channel(
+                channel_id, code=channel_data.drop_code,
+                message=channel_data.drop_message)
+
+    def notify_reader_done(self):
+        """This method is called by the reader thread when the reader has
+        finished.
+        """
+
+        # Terminate all logical connections
+        self._logger.debug('termiating all logical connections...')
+        self._logical_channels_condition.acquire()
+        for channel_data in self._logical_channels.values():
+            try:
+                channel_data.request.connection.set_read_state(
+                    _LogicalConnection.STATE_TERMINATED)
+            except Exception:
+                pass
+        self._logical_channels_condition.release()
+
+    def fail_physical_connection(self, code, message):
+        """Fail the physical connection.
+
+        Args:
+            code: drop reason code.
+            message: drop message.
+        """
+
+        self._logger.debug('Failing the physical connection...')
+        self._send_drop_channel(_CONTROL_CHANNEL_ID, code, message)
+        self.physical_stream.close_connection(
+            common.STATUS_INTERNAL_ENDPOINT_ERROR)
+
+    def fail_logical_channel(self, channel_id, code, message):
+        """Fail a logical channel.
+
+        Args:
+            channel_id: channel id.
+            code: drop reason code.
+            message: drop message.
+        """
+
+        self._logger.debug('Failing logical channel %d...' % channel_id)
+        try:
+            self._logical_channels_condition.acquire()
+            if channel_id in self._logical_channels:
+                channel_data = self._logical_channels[channel_id]
+                # Close the logical channel. notify_worker_done() will be
+                # called later and it will send DropChannel.
+                channel_data.drop_code = code
+                channel_data.drop_message = message
+                channel_data.request.connection.set_read_state(
+                    _LogicalConnection.STATE_TERMINATED)
+            else:
+                self._send_drop_channel(channel_id, code, message)
+        finally:
+            self._logical_channels_condition.release()
+
+
+def use_mux(request):
+    return hasattr(request, 'mux') and request.mux
+
+
+def start(request, dispatcher):
+    mux_handler = _MuxHandler(request, dispatcher)
+    mux_handler.start()
+
+    mux_handler.add_channel_slots(_INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+                                  _INITIAL_QUOTA_FOR_CLIENT)
+
+    mux_handler.wait_until_done()
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/standalone.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/standalone.py
new file mode 100755
index 0000000..07a33d9
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/standalone.py
@@ -0,0 +1,998 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, 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.
+
+
+"""Standalone WebSocket server.
+
+Use this file to launch pywebsocket without Apache HTTP Server.
+
+
+BASIC USAGE
+
+Go to the src directory and run
+
+  $ python mod_pywebsocket/standalone.py [-p <ws_port>]
+                                         [-w <websock_handlers>]
+                                         [-d <document_root>]
+
+<ws_port> is the port number to use for ws:// connection.
+
+<document_root> is the path to the root directory of HTML files.
+
+<websock_handlers> is the path to the root directory of WebSocket handlers.
+If not specified, <document_root> will be used. See __init__.py (or
+run $ pydoc mod_pywebsocket) for how to write WebSocket handlers.
+
+For more detail and other options, run
+
+  $ python mod_pywebsocket/standalone.py --help
+
+or see _build_option_parser method below.
+
+For trouble shooting, adding "--log_level debug" might help you.
+
+
+TRY DEMO
+
+Go to the src directory and run
+
+  $ python standalone.py -d example
+
+to launch pywebsocket with the sample handler and html on port 80. Open
+http://localhost/console.html, click the connect button, type something into
+the text box next to the send button and click the send button. If everything
+is working, you'll see the message you typed echoed by the server.
+
+
+SUPPORTING TLS
+
+To support TLS, run standalone.py with -t, -k, and -c options.
+
+
+SUPPORTING CLIENT AUTHENTICATION
+
+To support client authentication with TLS, run standalone.py with -t, -k, -c,
+and --tls-client-auth, and --tls-client-ca options.
+
+E.g., $./standalone.py -d ../example -p 10443 -t -c ../test/cert/cert.pem -k
+../test/cert/key.pem --tls-client-auth --tls-client-ca=../test/cert/cacert.pem
+
+
+CONFIGURATION FILE
+
+You can also write a configuration file and use it by specifying the path to
+the configuration file by --config option. Please write a configuration file
+following the documentation of the Python ConfigParser library. Name of each
+entry must be the long version argument name. E.g. to set log level to debug,
+add the following line:
+
+log_level=debug
+
+For options which doesn't take value, please add some fake value. E.g. for
+--tls option, add the following line:
+
+tls=True
+
+Note that tls will be enabled even if you write tls=False as the value part is
+fake.
+
+When both a command line argument and a configuration file entry are set for
+the same configuration item, the command line value will override one in the
+configuration file.
+
+
+THREADING
+
+This server is derived from SocketServer.ThreadingMixIn. Hence a thread is
+used for each request.
+
+
+SECURITY WARNING
+
+This uses CGIHTTPServer and CGIHTTPServer is not secure.
+It may execute arbitrary Python code or external programs. It should not be
+used outside a firewall.
+"""
+
+import BaseHTTPServer
+import CGIHTTPServer
+import SimpleHTTPServer
+import SocketServer
+import ConfigParser
+import base64
+import httplib
+import logging
+import logging.handlers
+import optparse
+import os
+import re
+import select
+import socket
+import sys
+import threading
+import time
+
+_HAS_SSL = False
+_HAS_OPEN_SSL = False
+try:
+    import ssl
+    _HAS_SSL = True
+except ImportError:
+    try:
+        import OpenSSL.SSL
+        _HAS_OPEN_SSL = True
+    except ImportError:
+        pass
+
+from mod_pywebsocket import common
+from mod_pywebsocket import dispatch
+from mod_pywebsocket import handshake
+from mod_pywebsocket import http_header_util
+from mod_pywebsocket import memorizingfile
+from mod_pywebsocket import util
+
+
+_DEFAULT_LOG_MAX_BYTES = 1024 * 256
+_DEFAULT_LOG_BACKUP_COUNT = 5
+
+_DEFAULT_REQUEST_QUEUE_SIZE = 128
+
+# 1024 is practically large enough to contain WebSocket handshake lines.
+_MAX_MEMORIZED_LINES = 1024
+
+
+class _StandaloneConnection(object):
+    """Mimic mod_python mp_conn."""
+
+    def __init__(self, request_handler):
+        """Construct an instance.
+
+        Args:
+            request_handler: A WebSocketRequestHandler instance.
+        """
+
+        self._request_handler = request_handler
+
+    def get_local_addr(self):
+        """Getter to mimic mp_conn.local_addr."""
+
+        return (self._request_handler.server.server_name,
+                self._request_handler.server.server_port)
+    local_addr = property(get_local_addr)
+
+    def get_remote_addr(self):
+        """Getter to mimic mp_conn.remote_addr.
+
+        Setting the property in __init__ won't work because the request
+        handler is not initialized yet there."""
+
+        return self._request_handler.client_address
+    remote_addr = property(get_remote_addr)
+
+    def write(self, data):
+        """Mimic mp_conn.write()."""
+
+        return self._request_handler.wfile.write(data)
+
+    def read(self, length):
+        """Mimic mp_conn.read()."""
+
+        return self._request_handler.rfile.read(length)
+
+    def get_memorized_lines(self):
+        """Get memorized lines."""
+
+        return self._request_handler.rfile.get_memorized_lines()
+
+
+class _StandaloneRequest(object):
+    """Mimic mod_python request."""
+
+    def __init__(self, request_handler, use_tls):
+        """Construct an instance.
+
+        Args:
+            request_handler: A WebSocketRequestHandler instance.
+        """
+
+        self._logger = util.get_class_logger(self)
+
+        self._request_handler = request_handler
+        self.connection = _StandaloneConnection(request_handler)
+        self._use_tls = use_tls
+        self.headers_in = request_handler.headers
+
+    def get_uri(self):
+        """Getter to mimic request.uri."""
+
+        return self._request_handler.path
+    uri = property(get_uri)
+
+    def get_method(self):
+        """Getter to mimic request.method."""
+
+        return self._request_handler.command
+    method = property(get_method)
+
+    def get_protocol(self):
+        """Getter to mimic request.protocol."""
+
+        return self._request_handler.request_version
+    protocol = property(get_protocol)
+
+    def is_https(self):
+        """Mimic request.is_https()."""
+
+        return self._use_tls
+
+    def _drain_received_data(self):
+        """Don't use this method from WebSocket handler. Drains unread data
+        in the receive buffer.
+        """
+
+        raw_socket = self._request_handler.connection
+        drained_data = util.drain_received_data(raw_socket)
+
+        if drained_data:
+            self._logger.debug(
+                'Drained data following close frame: %r', drained_data)
+
+
+class _StandaloneSSLConnection(object):
+    """A wrapper class for OpenSSL.SSL.Connection to provide makefile method
+    which is not supported by the class.
+    """
+
+    def __init__(self, connection):
+        self._connection = connection
+
+    def __getattribute__(self, name):
+        if name in ('_connection', 'makefile'):
+            return object.__getattribute__(self, name)
+        return self._connection.__getattribute__(name)
+
+    def __setattr__(self, name, value):
+        if name in ('_connection', 'makefile'):
+            return object.__setattr__(self, name, value)
+        return self._connection.__setattr__(name, value)
+
+    def makefile(self, mode='r', bufsize=-1):
+        return socket._fileobject(self._connection, mode, bufsize)
+
+
+def _alias_handlers(dispatcher, websock_handlers_map_file):
+    """Set aliases specified in websock_handler_map_file in dispatcher.
+
+    Args:
+        dispatcher: dispatch.Dispatcher instance
+        websock_handler_map_file: alias map file
+    """
+
+    fp = open(websock_handlers_map_file)
+    try:
+        for line in fp:
+            if line[0] == '#' or line.isspace():
+                continue
+            m = re.match('(\S+)\s+(\S+)', line)
+            if not m:
+                logging.warning('Wrong format in map file:' + line)
+                continue
+            try:
+                dispatcher.add_resource_path_alias(
+                    m.group(1), m.group(2))
+            except dispatch.DispatchException, e:
+                logging.error(str(e))
+    finally:
+        fp.close()
+
+
+class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
+    """HTTPServer specialized for WebSocket."""
+
+    # Overrides SocketServer.ThreadingMixIn.daemon_threads
+    daemon_threads = True
+    # Overrides BaseHTTPServer.HTTPServer.allow_reuse_address
+    allow_reuse_address = True
+
+    def __init__(self, options):
+        """Override SocketServer.TCPServer.__init__ to set SSL enabled
+        socket object to self.socket before server_bind and server_activate,
+        if necessary.
+        """
+
+        # Share a Dispatcher among request handlers to save time for
+        # instantiation.  Dispatcher can be shared because it is thread-safe.
+        options.dispatcher = dispatch.Dispatcher(
+            options.websock_handlers,
+            options.scan_dir,
+            options.allow_handlers_outside_root_dir)
+        if options.websock_handlers_map_file:
+            _alias_handlers(options.dispatcher,
+                            options.websock_handlers_map_file)
+        warnings = options.dispatcher.source_warnings()
+        if warnings:
+            for warning in warnings:
+                logging.warning('mod_pywebsocket: %s' % warning)
+
+        self._logger = util.get_class_logger(self)
+
+        self.request_queue_size = options.request_queue_size
+        self.__ws_is_shut_down = threading.Event()
+        self.__ws_serving = False
+
+        SocketServer.BaseServer.__init__(
+            self, (options.server_host, options.port), WebSocketRequestHandler)
+
+        # Expose the options object to allow handler objects access it. We name
+        # it with websocket_ prefix to avoid conflict.
+        self.websocket_server_options = options
+
+        self._create_sockets()
+        self.server_bind()
+        self.server_activate()
+
+    def _create_sockets(self):
+        self.server_name, self.server_port = self.server_address
+        self._sockets = []
+        if not self.server_name:
+            # On platforms that doesn't support IPv6, the first bind fails.
+            # On platforms that supports IPv6
+            # - If it binds both IPv4 and IPv6 on call with AF_INET6, the
+            #   first bind succeeds and the second fails (we'll see 'Address
+            #   already in use' error).
+            # - If it binds only IPv6 on call with AF_INET6, both call are
+            #   expected to succeed to listen both protocol.
+            addrinfo_array = [
+                (socket.AF_INET6, socket.SOCK_STREAM, '', '', ''),
+                (socket.AF_INET, socket.SOCK_STREAM, '', '', '')]
+        else:
+            addrinfo_array = socket.getaddrinfo(self.server_name,
+                                                self.server_port,
+                                                socket.AF_UNSPEC,
+                                                socket.SOCK_STREAM,
+                                                socket.IPPROTO_TCP)
+        for addrinfo in addrinfo_array:
+            self._logger.info('Create socket on: %r', addrinfo)
+            family, socktype, proto, canonname, sockaddr = addrinfo
+            try:
+                socket_ = socket.socket(family, socktype)
+            except Exception, e:
+                self._logger.info('Skip by failure: %r', e)
+                continue
+            if self.websocket_server_options.use_tls:
+                if _HAS_SSL:
+                    if self.websocket_server_options.tls_client_auth:
+                        client_cert_ = ssl.CERT_REQUIRED
+                    else:
+                        client_cert_ = ssl.CERT_NONE
+                    socket_ = ssl.wrap_socket(socket_,
+                        keyfile=self.websocket_server_options.private_key,
+                        certfile=self.websocket_server_options.certificate,
+                        ssl_version=ssl.PROTOCOL_SSLv23,
+                        ca_certs=self.websocket_server_options.tls_client_ca,
+                        cert_reqs=client_cert_)
+                if _HAS_OPEN_SSL:
+                    ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
+                    ctx.use_privatekey_file(
+                        self.websocket_server_options.private_key)
+                    ctx.use_certificate_file(
+                        self.websocket_server_options.certificate)
+                    socket_ = OpenSSL.SSL.Connection(ctx, socket_)
+            self._sockets.append((socket_, addrinfo))
+
+    def server_bind(self):
+        """Override SocketServer.TCPServer.server_bind to enable multiple
+        sockets bind.
+        """
+
+        failed_sockets = []
+
+        for socketinfo in self._sockets:
+            socket_, addrinfo = socketinfo
+            self._logger.info('Bind on: %r', addrinfo)
+            if self.allow_reuse_address:
+                socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+            try:
+                socket_.bind(self.server_address)
+            except Exception, e:
+                self._logger.info('Skip by failure: %r', e)
+                socket_.close()
+                failed_sockets.append(socketinfo)
+            if self.server_address[1] == 0:
+                # The operating system assigns the actual port number for port
+                # number 0. This case, the second and later sockets should use
+                # the same port number. Also self.server_port is rewritten
+                # because it is exported, and will be used by external code.
+                self.server_address = (
+                    self.server_name, socket_.getsockname()[1])
+                self.server_port = self.server_address[1]
+                self._logger.info('Port %r is assigned', self.server_port)
+
+        for socketinfo in failed_sockets:
+            self._sockets.remove(socketinfo)
+
+    def server_activate(self):
+        """Override SocketServer.TCPServer.server_activate to enable multiple
+        sockets listen.
+        """
+
+        failed_sockets = []
+
+        for socketinfo in self._sockets:
+            socket_, addrinfo = socketinfo
+            self._logger.info('Listen on: %r', addrinfo)
+            try:
+                socket_.listen(self.request_queue_size)
+            except Exception, e:
+                self._logger.info('Skip by failure: %r', e)
+                socket_.close()
+                failed_sockets.append(socketinfo)
+
+        for socketinfo in failed_sockets:
+            self._sockets.remove(socketinfo)
+
+        if len(self._sockets) == 0:
+            self._logger.critical(
+                'No sockets activated. Use info log level to see the reason.')
+
+    def server_close(self):
+        """Override SocketServer.TCPServer.server_close to enable multiple
+        sockets close.
+        """
+
+        for socketinfo in self._sockets:
+            socket_, addrinfo = socketinfo
+            self._logger.info('Close on: %r', addrinfo)
+            socket_.close()
+
+    def fileno(self):
+        """Override SocketServer.TCPServer.fileno."""
+
+        self._logger.critical('Not supported: fileno')
+        return self._sockets[0][0].fileno()
+
+    def handle_error(self, rquest, client_address):
+        """Override SocketServer.handle_error."""
+
+        self._logger.error(
+            'Exception in processing request from: %r\n%s',
+            client_address,
+            util.get_stack_trace())
+        # Note: client_address is a tuple.
+
+    def get_request(self):
+        """Override TCPServer.get_request to wrap OpenSSL.SSL.Connection
+        object with _StandaloneSSLConnection to provide makefile method. We
+        cannot substitute OpenSSL.SSL.Connection.makefile since it's readonly
+        attribute.
+        """
+
+        accepted_socket, client_address = self.socket.accept()
+        if self.websocket_server_options.use_tls and _HAS_OPEN_SSL:
+            accepted_socket = _StandaloneSSLConnection(accepted_socket)
+        return accepted_socket, client_address
+
+    def serve_forever(self, poll_interval=0.5):
+        """Override SocketServer.BaseServer.serve_forever."""
+
+        self.__ws_serving = True
+        self.__ws_is_shut_down.clear()
+        handle_request = self.handle_request
+        if hasattr(self, '_handle_request_noblock'):
+            handle_request = self._handle_request_noblock
+        else:
+            self._logger.warning('Fallback to blocking request handler')
+        try:
+            while self.__ws_serving:
+                r, w, e = select.select(
+                    [socket_[0] for socket_ in self._sockets],
+                    [], [], poll_interval)
+                for socket_ in r:
+                    self.socket = socket_
+                    handle_request()
+                self.socket = None
+        finally:
+            self.__ws_is_shut_down.set()
+
+    def shutdown(self):
+        """Override SocketServer.BaseServer.shutdown."""
+
+        self.__ws_serving = False
+        self.__ws_is_shut_down.wait()
+
+
+class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
+    """CGIHTTPRequestHandler specialized for WebSocket."""
+
+    # Use httplib.HTTPMessage instead of mimetools.Message.
+    MessageClass = httplib.HTTPMessage
+
+    def setup(self):
+        """Override SocketServer.StreamRequestHandler.setup to wrap rfile
+        with MemorizingFile.
+
+        This method will be called by BaseRequestHandler's constructor
+        before calling BaseHTTPRequestHandler.handle.
+        BaseHTTPRequestHandler.handle will call
+        BaseHTTPRequestHandler.handle_one_request and it will call
+        WebSocketRequestHandler.parse_request.
+        """
+
+        # Call superclass's setup to prepare rfile, wfile, etc. See setup
+        # definition on the root class SocketServer.StreamRequestHandler to
+        # understand what this does.
+        CGIHTTPServer.CGIHTTPRequestHandler.setup(self)
+
+        self.rfile = memorizingfile.MemorizingFile(
+            self.rfile,
+            max_memorized_lines=_MAX_MEMORIZED_LINES)
+
+    def __init__(self, request, client_address, server):
+        self._logger = util.get_class_logger(self)
+
+        self._options = server.websocket_server_options
+
+        # Overrides CGIHTTPServerRequestHandler.cgi_directories.
+        self.cgi_directories = self._options.cgi_directories
+        # Replace CGIHTTPRequestHandler.is_executable method.
+        if self._options.is_executable_method is not None:
+            self.is_executable = self._options.is_executable_method
+
+        # This actually calls BaseRequestHandler.__init__.
+        CGIHTTPServer.CGIHTTPRequestHandler.__init__(
+            self, request, client_address, server)
+
+    def parse_request(self):
+        """Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request.
+
+        Return True to continue processing for HTTP(S), False otherwise.
+
+        See BaseHTTPRequestHandler.handle_one_request method which calls
+        this method to understand how the return value will be handled.
+        """
+
+        # We hook parse_request method, but also call the original
+        # CGIHTTPRequestHandler.parse_request since when we return False,
+        # CGIHTTPRequestHandler.handle_one_request continues processing and
+        # it needs variables set by CGIHTTPRequestHandler.parse_request.
+        #
+        # Variables set by this method will be also used by WebSocket request
+        # handling (self.path, self.command, self.requestline, etc. See also
+        # how _StandaloneRequest's members are implemented using these
+        # attributes).
+        if not CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self):
+            return False
+
+        if self._options.use_basic_auth:
+            auth = self.headers.getheader('Authorization')
+            if auth != self._options.basic_auth_credential:
+                self.send_response(401)
+                self.send_header('WWW-Authenticate',
+                                 'Basic realm="Pywebsocket"')
+                self.end_headers()
+                self._logger.info('Request basic authentication')
+                return True
+
+        host, port, resource = http_header_util.parse_uri(self.path)
+        if resource is None:
+            self._logger.info('Invalid URI: %r', self.path)
+            self._logger.info('Fallback to CGIHTTPRequestHandler')
+            return True
+        server_options = self.server.websocket_server_options
+        if host is not None:
+            validation_host = server_options.validation_host
+            if validation_host is not None and host != validation_host:
+                self._logger.info('Invalid host: %r (expected: %r)',
+                                  host,
+                                  validation_host)
+                self._logger.info('Fallback to CGIHTTPRequestHandler')
+                return True
+        if port is not None:
+            validation_port = server_options.validation_port
+            if validation_port is not None and port != validation_port:
+                self._logger.info('Invalid port: %r (expected: %r)',
+                                  port,
+                                  validation_port)
+                self._logger.info('Fallback to CGIHTTPRequestHandler')
+                return True
+        self.path = resource
+
+        request = _StandaloneRequest(self, self._options.use_tls)
+
+        try:
+            # Fallback to default http handler for request paths for which
+            # we don't have request handlers.
+            if not self._options.dispatcher.get_handler_suite(self.path):
+                self._logger.info('No handler for resource: %r',
+                                  self.path)
+                self._logger.info('Fallback to CGIHTTPRequestHandler')
+                return True
+        except dispatch.DispatchException, e:
+            self._logger.info('%s', e)
+            self.send_error(e.status)
+            return False
+
+        # If any Exceptions without except clause setup (including
+        # DispatchException) is raised below this point, it will be caught
+        # and logged by WebSocketServer.
+
+        try:
+            try:
+                handshake.do_handshake(
+                    request,
+                    self._options.dispatcher,
+                    allowDraft75=self._options.allow_draft75,
+                    strict=self._options.strict)
+            except handshake.VersionException, e:
+                self._logger.info('%s', e)
+                self.send_response(common.HTTP_STATUS_BAD_REQUEST)
+                self.send_header(common.SEC_WEBSOCKET_VERSION_HEADER,
+                                 e.supported_versions)
+                self.end_headers()
+                return False
+            except handshake.HandshakeException, e:
+                # Handshake for ws(s) failed.
+                self._logger.info('%s', e)
+                self.send_error(e.status)
+                return False
+
+            request._dispatcher = self._options.dispatcher
+            self._options.dispatcher.transfer_data(request)
+        except handshake.AbortedByUserException, e:
+            self._logger.info('%s', e)
+        return False
+
+    def log_request(self, code='-', size='-'):
+        """Override BaseHTTPServer.log_request."""
+
+        self._logger.info('"%s" %s %s',
+                          self.requestline, str(code), str(size))
+
+    def log_error(self, *args):
+        """Override BaseHTTPServer.log_error."""
+
+        # Despite the name, this method is for warnings than for errors.
+        # For example, HTTP status code is logged by this method.
+        self._logger.warning('%s - %s',
+                             self.address_string(),
+                             args[0] % args[1:])
+
+    def is_cgi(self):
+        """Test whether self.path corresponds to a CGI script.
+
+        Add extra check that self.path doesn't contains ..
+        Also check if the file is a executable file or not.
+        If the file is not executable, it is handled as static file or dir
+        rather than a CGI script.
+        """
+
+        if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self):
+            if '..' in self.path:
+                return False
+            # strip query parameter from request path
+            resource_name = self.path.split('?', 2)[0]
+            # convert resource_name into real path name in filesystem.
+            scriptfile = self.translate_path(resource_name)
+            if not os.path.isfile(scriptfile):
+                return False
+            if not self.is_executable(scriptfile):
+                return False
+            return True
+        return False
+
+
+def _get_logger_from_class(c):
+    return logging.getLogger('%s.%s' % (c.__module__, c.__name__))
+
+
+def _configure_logging(options):
+    logging.addLevelName(common.LOGLEVEL_FINE, 'FINE')
+
+    logger = logging.getLogger()
+    logger.setLevel(logging.getLevelName(options.log_level.upper()))
+    if options.log_file:
+        handler = logging.handlers.RotatingFileHandler(
+                options.log_file, 'a', options.log_max, options.log_count)
+    else:
+        handler = logging.StreamHandler()
+    formatter = logging.Formatter(
+            '[%(asctime)s] [%(levelname)s] %(name)s: %(message)s')
+    handler.setFormatter(formatter)
+    logger.addHandler(handler)
+
+    deflate_log_level_name = logging.getLevelName(
+        options.deflate_log_level.upper())
+    _get_logger_from_class(util._Deflater).setLevel(
+        deflate_log_level_name)
+    _get_logger_from_class(util._Inflater).setLevel(
+        deflate_log_level_name)
+
+
+def _build_option_parser():
+    parser = optparse.OptionParser()
+
+    parser.add_option('--config', dest='config_file', type='string',
+                      default=None,
+                      help=('Path to configuration file. See the file comment '
+                            'at the top of this file for the configuration '
+                            'file format'))
+    parser.add_option('-H', '--server-host', '--server_host',
+                      dest='server_host',
+                      default='',
+                      help='server hostname to listen to')
+    parser.add_option('-V', '--validation-host', '--validation_host',
+                      dest='validation_host',
+                      default=None,
+                      help='server hostname to validate in absolute path.')
+    parser.add_option('-p', '--port', dest='port', type='int',
+                      default=common.DEFAULT_WEB_SOCKET_PORT,
+                      help='port to listen to')
+    parser.add_option('-P', '--validation-port', '--validation_port',
+                      dest='validation_port', type='int',
+                      default=None,
+                      help='server port to validate in absolute path.')
+    parser.add_option('-w', '--websock-handlers', '--websock_handlers',
+                      dest='websock_handlers',
+                      default='.',
+                      help=('The root directory of WebSocket handler files. '
+                            'If the path is relative, --document-root is used '
+                            'as the base.'))
+    parser.add_option('-m', '--websock-handlers-map-file',
+                      '--websock_handlers_map_file',
+                      dest='websock_handlers_map_file',
+                      default=None,
+                      help=('WebSocket handlers map file. '
+                            'Each line consists of alias_resource_path and '
+                            'existing_resource_path, separated by spaces.'))
+    parser.add_option('-s', '--scan-dir', '--scan_dir', dest='scan_dir',
+                      default=None,
+                      help=('Must be a directory under --websock-handlers. '
+                            'Only handlers under this directory are scanned '
+                            'and registered to the server. '
+                            'Useful for saving scan time when the handler '
+                            'root directory contains lots of files that are '
+                            'not handler file or are handler files but you '
+                            'don\'t want them to be registered. '))
+    parser.add_option('--allow-handlers-outside-root-dir',
+                      '--allow_handlers_outside_root_dir',
+                      dest='allow_handlers_outside_root_dir',
+                      action='store_true',
+                      default=False,
+                      help=('Scans WebSocket handlers even if their canonical '
+                            'path is not under --websock-handlers.'))
+    parser.add_option('-d', '--document-root', '--document_root',
+                      dest='document_root', default='.',
+                      help='Document root directory.')
+    parser.add_option('-x', '--cgi-paths', '--cgi_paths', dest='cgi_paths',
+                      default=None,
+                      help=('CGI paths relative to document_root.'
+                            'Comma-separated. (e.g -x /cgi,/htbin) '
+                            'Files under document_root/cgi_path are handled '
+                            'as CGI programs. Must be executable.'))
+    parser.add_option('-t', '--tls', dest='use_tls', action='store_true',
+                      default=False, help='use TLS (wss://)')
+    parser.add_option('-k', '--private-key', '--private_key',
+                      dest='private_key',
+                      default='', help='TLS private key file.')
+    parser.add_option('-c', '--certificate', dest='certificate',
+                      default='', help='TLS certificate file.')
+    parser.add_option('--tls-client-auth', dest='tls_client_auth',
+                      action='store_true', default=False,
+                      help='Requires TLS client auth on every connection.')
+    parser.add_option('--tls-client-ca', dest='tls_client_ca', default='',
+                      help=('Specifies a pem file which contains a set of '
+                            'concatenated CA certificates which are used to '
+                            'validate certificates passed from clients'))
+    parser.add_option('--basic-auth', dest='use_basic_auth',
+                      action='store_true', default=False,
+                      help='Requires Basic authentication.')
+    parser.add_option('--basic-auth-credential',
+                      dest='basic_auth_credential', default='test:test',
+                      help='Specifies the credential of basic authentication '
+                      'by username:password pair (e.g. test:test).')
+    parser.add_option('-l', '--log-file', '--log_file', dest='log_file',
+                      default='', help='Log file.')
+    # Custom log level:
+    # - FINE: Prints status of each frame processing step
+    parser.add_option('--log-level', '--log_level', type='choice',
+                      dest='log_level', default='warn',
+                      choices=['fine',
+                               'debug', 'info', 'warning', 'warn', 'error',
+                               'critical'],
+                      help='Log level.')
+    parser.add_option('--deflate-log-level', '--deflate_log_level',
+                      type='choice',
+                      dest='deflate_log_level', default='warn',
+                      choices=['debug', 'info', 'warning', 'warn', 'error',
+                               'critical'],
+                      help='Log level for _Deflater and _Inflater.')
+    parser.add_option('--thread-monitor-interval-in-sec',
+                      '--thread_monitor_interval_in_sec',
+                      dest='thread_monitor_interval_in_sec',
+                      type='int', default=-1,
+                      help=('If positive integer is specified, run a thread '
+                            'monitor to show the status of server threads '
+                            'periodically in the specified inteval in '
+                            'second. If non-positive integer is specified, '
+                            'disable the thread monitor.'))
+    parser.add_option('--log-max', '--log_max', dest='log_max', type='int',
+                      default=_DEFAULT_LOG_MAX_BYTES,
+                      help='Log maximum bytes')
+    parser.add_option('--log-count', '--log_count', dest='log_count',
+                      type='int', default=_DEFAULT_LOG_BACKUP_COUNT,
+                      help='Log backup count')
+    parser.add_option('--allow-draft75', dest='allow_draft75',
+                      action='store_true', default=False,
+                      help='Obsolete option. Ignored.')
+    parser.add_option('--strict', dest='strict', action='store_true',
+                      default=False, help='Obsolete option. Ignored.')
+    parser.add_option('-q', '--queue', dest='request_queue_size', type='int',
+                      default=_DEFAULT_REQUEST_QUEUE_SIZE,
+                      help='request queue size')
+
+    return parser
+
+
+class ThreadMonitor(threading.Thread):
+    daemon = True
+
+    def __init__(self, interval_in_sec):
+        threading.Thread.__init__(self, name='ThreadMonitor')
+
+        self._logger = util.get_class_logger(self)
+
+        self._interval_in_sec = interval_in_sec
+
+    def run(self):
+        while True:
+            thread_name_list = []
+            for thread in threading.enumerate():
+                thread_name_list.append(thread.name)
+            self._logger.info(
+                "%d active threads: %s",
+                threading.active_count(),
+                ', '.join(thread_name_list))
+            time.sleep(self._interval_in_sec)
+
+
+def _parse_args_and_config(args):
+    parser = _build_option_parser()
+
+    # First, parse options without configuration file.
+    temporary_options, temporary_args = parser.parse_args(args=args)
+    if temporary_args:
+        logging.critical(
+            'Unrecognized positional arguments: %r', temporary_args)
+        sys.exit(1)
+
+    if temporary_options.config_file:
+        try:
+            config_fp = open(temporary_options.config_file, 'r')
+        except IOError, e:
+            logging.critical(
+                'Failed to open configuration file %r: %r',
+                temporary_options.config_file,
+                e)
+            sys.exit(1)
+
+        config_parser = ConfigParser.SafeConfigParser()
+        config_parser.readfp(config_fp)
+        config_fp.close()
+
+        args_from_config = []
+        for name, value in config_parser.items('pywebsocket'):
+            args_from_config.append('--' + name)
+            args_from_config.append(value)
+        if args is None:
+            args = args_from_config
+        else:
+            args = args_from_config + args
+        return parser.parse_args(args=args)
+    else:
+        return temporary_options, temporary_args
+
+
+def _main(args=None):
+    """You can call this function from your own program, but please note that
+    this function has some side-effects that might affect your program. For
+    example, util.wrap_popen3_for_win use in this method replaces implementation
+    of os.popen3.
+    """
+
+    options, args = _parse_args_and_config(args=args)
+
+    os.chdir(options.document_root)
+
+    _configure_logging(options)
+
+    # TODO(tyoshino): Clean up initialization of CGI related values. Move some
+    # of code here to WebSocketRequestHandler class if it's better.
+    options.cgi_directories = []
+    options.is_executable_method = None
+    if options.cgi_paths:
+        options.cgi_directories = options.cgi_paths.split(',')
+        if sys.platform in ('cygwin', 'win32'):
+            cygwin_path = None
+            # For Win32 Python, it is expected that CYGWIN_PATH
+            # is set to a directory of cygwin binaries.
+            # For example, websocket_server.py in Chromium sets CYGWIN_PATH to
+            # full path of third_party/cygwin/bin.
+            if 'CYGWIN_PATH' in os.environ:
+                cygwin_path = os.environ['CYGWIN_PATH']
+            util.wrap_popen3_for_win(cygwin_path)
+
+            def __check_script(scriptpath):
+                return util.get_script_interp(scriptpath, cygwin_path)
+
+            options.is_executable_method = __check_script
+
+    if options.use_tls:
+        if not (_HAS_SSL or _HAS_OPEN_SSL):
+            logging.critical('TLS support requires ssl or pyOpenSSL module.')
+            sys.exit(1)
+        if not options.private_key or not options.certificate:
+            logging.critical(
+                    'To use TLS, specify private_key and certificate.')
+            sys.exit(1)
+
+    if options.tls_client_auth:
+        if not options.use_tls:
+            logging.critical('TLS must be enabled for client authentication.')
+            sys.exit(1)
+        if not _HAS_SSL:
+            logging.critical('Client authentication requires ssl module.')
+
+    if not options.scan_dir:
+        options.scan_dir = options.websock_handlers
+
+    if options.use_basic_auth:
+        options.basic_auth_credential = 'Basic ' + base64.b64encode(
+            options.basic_auth_credential)
+
+    try:
+        if options.thread_monitor_interval_in_sec > 0:
+            # Run a thread monitor to show the status of server threads for
+            # debugging.
+            ThreadMonitor(options.thread_monitor_interval_in_sec).start()
+
+        server = WebSocketServer(options)
+        server.serve_forever()
+    except Exception, e:
+        logging.critical('mod_pywebsocket: %s' % e)
+        logging.critical('mod_pywebsocket: %s' % util.get_stack_trace())
+        sys.exit(1)
+
+
+if __name__ == '__main__':
+    _main(sys.argv[1:])
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/stream.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/stream.py
new file mode 100644
index 0000000..edc5332
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/stream.py
@@ -0,0 +1,57 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""This file exports public symbols.
+"""
+
+
+from mod_pywebsocket._stream_base import BadOperationException
+from mod_pywebsocket._stream_base import ConnectionTerminatedException
+from mod_pywebsocket._stream_base import InvalidFrameException
+from mod_pywebsocket._stream_base import InvalidUTF8Exception
+from mod_pywebsocket._stream_base import UnsupportedFrameException
+from mod_pywebsocket._stream_hixie75 import StreamHixie75
+from mod_pywebsocket._stream_hybi import Frame
+from mod_pywebsocket._stream_hybi import Stream
+from mod_pywebsocket._stream_hybi import StreamOptions
+
+# These methods are intended to be used by WebSocket client developers to have
+# their implementations receive broken data in tests.
+from mod_pywebsocket._stream_hybi import create_close_frame
+from mod_pywebsocket._stream_hybi import create_header
+from mod_pywebsocket._stream_hybi import create_length_header
+from mod_pywebsocket._stream_hybi import create_ping_frame
+from mod_pywebsocket._stream_hybi import create_pong_frame
+from mod_pywebsocket._stream_hybi import create_binary_frame
+from mod_pywebsocket._stream_hybi import create_text_frame
+from mod_pywebsocket._stream_hybi import create_closing_handshake_body
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/util.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/util.py
new file mode 100644
index 0000000..7bb0b5d
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/util.py
@@ -0,0 +1,515 @@
+# Copyright 2011, 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.
+
+
+"""WebSocket utilities.
+"""
+
+
+import array
+import errno
+
+# Import hash classes from a module available and recommended for each Python
+# version and re-export those symbol. Use sha and md5 module in Python 2.4, and
+# hashlib module in Python 2.6.
+try:
+    import hashlib
+    md5_hash = hashlib.md5
+    sha1_hash = hashlib.sha1
+except ImportError:
+    import md5
+    import sha
+    md5_hash = md5.md5
+    sha1_hash = sha.sha
+
+import StringIO
+import logging
+import os
+import re
+import socket
+import traceback
+import zlib
+
+
+def get_stack_trace():
+    """Get the current stack trace as string.
+
+    This is needed to support Python 2.3.
+    TODO: Remove this when we only support Python 2.4 and above.
+          Use traceback.format_exc instead.
+    """
+
+    out = StringIO.StringIO()
+    traceback.print_exc(file=out)
+    return out.getvalue()
+
+
+def prepend_message_to_exception(message, exc):
+    """Prepend message to the exception."""
+
+    exc.args = (message + str(exc),)
+    return
+
+
+def __translate_interp(interp, cygwin_path):
+    """Translate interp program path for Win32 python to run cygwin program
+    (e.g. perl).  Note that it doesn't support path that contains space,
+    which is typically true for Unix, where #!-script is written.
+    For Win32 python, cygwin_path is a directory of cygwin binaries.
+
+    Args:
+      interp: interp command line
+      cygwin_path: directory name of cygwin binary, or None
+    Returns:
+      translated interp command line.
+    """
+    if not cygwin_path:
+        return interp
+    m = re.match('^[^ ]*/([^ ]+)( .*)?', interp)
+    if m:
+        cmd = os.path.join(cygwin_path, m.group(1))
+        return cmd + m.group(2)
+    return interp
+
+
+def get_script_interp(script_path, cygwin_path=None):
+    """Gets #!-interpreter command line from the script.
+
+    It also fixes command path.  When Cygwin Python is used, e.g. in WebKit,
+    it could run "/usr/bin/perl -wT hello.pl".
+    When Win32 Python is used, e.g. in Chromium, it couldn't.  So, fix
+    "/usr/bin/perl" to "<cygwin_path>\perl.exe".
+
+    Args:
+      script_path: pathname of the script
+      cygwin_path: directory name of cygwin binary, or None
+    Returns:
+      #!-interpreter command line, or None if it is not #!-script.
+    """
+    fp = open(script_path)
+    line = fp.readline()
+    fp.close()
+    m = re.match('^#!(.*)', line)
+    if m:
+        return __translate_interp(m.group(1), cygwin_path)
+    return None
+
+
+def wrap_popen3_for_win(cygwin_path):
+    """Wrap popen3 to support #!-script on Windows.
+
+    Args:
+      cygwin_path:  path for cygwin binary if command path is needed to be
+                    translated.  None if no translation required.
+    """
+
+    __orig_popen3 = os.popen3
+
+    def __wrap_popen3(cmd, mode='t', bufsize=-1):
+        cmdline = cmd.split(' ')
+        interp = get_script_interp(cmdline[0], cygwin_path)
+        if interp:
+            cmd = interp + ' ' + cmd
+        return __orig_popen3(cmd, mode, bufsize)
+
+    os.popen3 = __wrap_popen3
+
+
+def hexify(s):
+    return ' '.join(map(lambda x: '%02x' % ord(x), s))
+
+
+def get_class_logger(o):
+    return logging.getLogger(
+        '%s.%s' % (o.__class__.__module__, o.__class__.__name__))
+
+
+class NoopMasker(object):
+    """A masking object that has the same interface as RepeatedXorMasker but
+    just returns the string passed in without making any change.
+    """
+
+    def __init__(self):
+        pass
+
+    def mask(self, s):
+        return s
+
+
+class RepeatedXorMasker(object):
+    """A masking object that applies XOR on the string given to mask method
+    with the masking bytes given to the constructor repeatedly. This object
+    remembers the position in the masking bytes the last mask method call
+    ended and resumes from that point on the next mask method call.
+    """
+
+    def __init__(self, mask):
+        self._mask = map(ord, mask)
+        self._mask_size = len(self._mask)
+        self._count = 0
+
+    def mask(self, s):
+        result = array.array('B')
+        result.fromstring(s)
+        # Use temporary local variables to eliminate the cost to access
+        # attributes
+        count = self._count
+        mask = self._mask
+        mask_size = self._mask_size
+        for i in xrange(len(result)):
+            result[i] ^= mask[count]
+            count = (count + 1) % mask_size
+        self._count = count
+
+        return result.tostring()
+
+
+class DeflateRequest(object):
+    """A wrapper class for request object to intercept send and recv to perform
+    deflate compression and decompression transparently.
+    """
+
+    def __init__(self, request):
+        self._request = request
+        self.connection = DeflateConnection(request.connection)
+
+    def __getattribute__(self, name):
+        if name in ('_request', 'connection'):
+            return object.__getattribute__(self, name)
+        return self._request.__getattribute__(name)
+
+    def __setattr__(self, name, value):
+        if name in ('_request', 'connection'):
+            return object.__setattr__(self, name, value)
+        return self._request.__setattr__(name, value)
+
+
+# By making wbits option negative, we can suppress CMF/FLG (2 octet) and
+# ADLER32 (4 octet) fields of zlib so that we can use zlib module just as
+# deflate library. DICTID won't be added as far as we don't set dictionary.
+# LZ77 window of 32K will be used for both compression and decompression.
+# For decompression, we can just use 32K to cover any windows size. For
+# compression, we use 32K so receivers must use 32K.
+#
+# Compression level is Z_DEFAULT_COMPRESSION. We don't have to match level
+# to decode.
+#
+# See zconf.h, deflate.cc, inflate.cc of zlib library, and zlibmodule.c of
+# Python. See also RFC1950 (ZLIB 3.3).
+
+
+class _Deflater(object):
+
+    def __init__(self, window_bits):
+        self._logger = get_class_logger(self)
+
+        self._compress = zlib.compressobj(
+            zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -window_bits)
+
+    def compress(self, bytes):
+        compressed_bytes = self._compress.compress(bytes)
+        self._logger.debug('Compress input %r', bytes)
+        self._logger.debug('Compress result %r', compressed_bytes)
+        return compressed_bytes
+
+    def compress_and_flush(self, bytes):
+        compressed_bytes = self._compress.compress(bytes)
+        compressed_bytes += self._compress.flush(zlib.Z_SYNC_FLUSH)
+        self._logger.debug('Compress input %r', bytes)
+        self._logger.debug('Compress result %r', compressed_bytes)
+        return compressed_bytes
+
+    def compress_and_finish(self, bytes):
+        compressed_bytes = self._compress.compress(bytes)
+        compressed_bytes += self._compress.flush(zlib.Z_FINISH)
+        self._logger.debug('Compress input %r', bytes)
+        self._logger.debug('Compress result %r', compressed_bytes)
+        return compressed_bytes
+
+class _Inflater(object):
+
+    def __init__(self):
+        self._logger = get_class_logger(self)
+
+        self._unconsumed = ''
+
+        self.reset()
+
+    def decompress(self, size):
+        if not (size == -1 or size > 0):
+            raise Exception('size must be -1 or positive')
+
+        data = ''
+
+        while True:
+            if size == -1:
+                data += self._decompress.decompress(self._unconsumed)
+                # See Python bug http://bugs.python.org/issue12050 to
+                # understand why the same code cannot be used for updating
+                # self._unconsumed for here and else block.
+                self._unconsumed = ''
+            else:
+                data += self._decompress.decompress(
+                    self._unconsumed, size - len(data))
+                self._unconsumed = self._decompress.unconsumed_tail
+            if self._decompress.unused_data:
+                # Encountered a last block (i.e. a block with BFINAL = 1) and
+                # found a new stream (unused_data). We cannot use the same
+                # zlib.Decompress object for the new stream. Create a new
+                # Decompress object to decompress the new one.
+                #
+                # It's fine to ignore unconsumed_tail if unused_data is not
+                # empty.
+                self._unconsumed = self._decompress.unused_data
+                self.reset()
+                if size >= 0 and len(data) == size:
+                    # data is filled. Don't call decompress again.
+                    break
+                else:
+                    # Re-invoke Decompress.decompress to try to decompress all
+                    # available bytes before invoking read which blocks until
+                    # any new byte is available.
+                    continue
+            else:
+                # Here, since unused_data is empty, even if unconsumed_tail is
+                # not empty, bytes of requested length are already in data. We
+                # don't have to "continue" here.
+                break
+
+        if data:
+            self._logger.debug('Decompressed %r', data)
+        return data
+
+    def append(self, data):
+        self._logger.debug('Appended %r', data)
+        self._unconsumed += data
+
+    def reset(self):
+        self._logger.debug('Reset')
+        self._decompress = zlib.decompressobj(-zlib.MAX_WBITS)
+
+
+# Compresses/decompresses given octets using the method introduced in RFC1979.
+
+
+class _RFC1979Deflater(object):
+    """A compressor class that applies DEFLATE to given byte sequence and
+    flushes using the algorithm described in the RFC1979 section 2.1.
+    """
+
+    def __init__(self, window_bits, no_context_takeover):
+        self._deflater = None
+        if window_bits is None:
+            window_bits = zlib.MAX_WBITS
+        self._window_bits = window_bits
+        self._no_context_takeover = no_context_takeover
+
+    def filter(self, bytes, flush=True, bfinal=False):
+        if self._deflater is None or (self._no_context_takeover and flush):
+            self._deflater = _Deflater(self._window_bits)
+
+        if bfinal:
+            result = self._deflater.compress_and_finish(bytes)
+            # Add a padding block with BFINAL = 0 and BTYPE = 0.
+            result = result + chr(0)
+            self._deflater = None
+            return result
+        if flush:
+            # Strip last 4 octets which is LEN and NLEN field of a
+            # non-compressed block added for Z_SYNC_FLUSH.
+            return self._deflater.compress_and_flush(bytes)[:-4]
+        return self._deflater.compress(bytes)
+
+class _RFC1979Inflater(object):
+    """A decompressor class for byte sequence compressed and flushed following
+    the algorithm described in the RFC1979 section 2.1.
+    """
+
+    def __init__(self):
+        self._inflater = _Inflater()
+
+    def filter(self, bytes):
+        # Restore stripped LEN and NLEN field of a non-compressed block added
+        # for Z_SYNC_FLUSH.
+        self._inflater.append(bytes + '\x00\x00\xff\xff')
+        return self._inflater.decompress(-1)
+
+
+class DeflateSocket(object):
+    """A wrapper class for socket object to intercept send and recv to perform
+    deflate compression and decompression transparently.
+    """
+
+    # Size of the buffer passed to recv to receive compressed data.
+    _RECV_SIZE = 4096
+
+    def __init__(self, socket):
+        self._socket = socket
+
+        self._logger = get_class_logger(self)
+
+        self._deflater = _Deflater(zlib.MAX_WBITS)
+        self._inflater = _Inflater()
+
+    def recv(self, size):
+        """Receives data from the socket specified on the construction up
+        to the specified size. Once any data is available, returns it even
+        if it's smaller than the specified size.
+        """
+
+        # TODO(tyoshino): Allow call with size=0. It should block until any
+        # decompressed data is available.
+        if size <= 0:
+            raise Exception('Non-positive size passed')
+        while True:
+            data = self._inflater.decompress(size)
+            if len(data) != 0:
+                return data
+
+            read_data = self._socket.recv(DeflateSocket._RECV_SIZE)
+            if not read_data:
+                return ''
+            self._inflater.append(read_data)
+
+    def sendall(self, bytes):
+        self.send(bytes)
+
+    def send(self, bytes):
+        self._socket.sendall(self._deflater.compress_and_flush(bytes))
+        return len(bytes)
+
+
+class DeflateConnection(object):
+    """A wrapper class for request object to intercept write and read to
+    perform deflate compression and decompression transparently.
+    """
+
+    def __init__(self, connection):
+        self._connection = connection
+
+        self._logger = get_class_logger(self)
+
+        self._deflater = _Deflater(zlib.MAX_WBITS)
+        self._inflater = _Inflater()
+
+    def get_remote_addr(self):
+        return self._connection.remote_addr
+    remote_addr = property(get_remote_addr)
+
+    def put_bytes(self, bytes):
+        self.write(bytes)
+
+    def read(self, size=-1):
+        """Reads at most size bytes. Blocks until there's at least one byte
+        available.
+        """
+
+        # TODO(tyoshino): Allow call with size=0.
+        if not (size == -1 or size > 0):
+            raise Exception('size must be -1 or positive')
+
+        data = ''
+        while True:
+            if size == -1:
+                data += self._inflater.decompress(-1)
+            else:
+                data += self._inflater.decompress(size - len(data))
+
+            if size >= 0 and len(data) != 0:
+                break
+
+            # TODO(tyoshino): Make this read efficient by some workaround.
+            #
+            # In 3.0.3 and prior of mod_python, read blocks until length bytes
+            # was read. We don't know the exact size to read while using
+            # deflate, so read byte-by-byte.
+            #
+            # _StandaloneRequest.read that ultimately performs
+            # socket._fileobject.read also blocks until length bytes was read
+            read_data = self._connection.read(1)
+            if not read_data:
+                break
+            self._inflater.append(read_data)
+        return data
+
+    def write(self, bytes):
+        self._connection.write(self._deflater.compress_and_flush(bytes))
+
+
+def _is_ewouldblock_errno(error_number):
+    """Returns True iff error_number indicates that receive operation would
+    block. To make this portable, we check availability of errno and then
+    compare them.
+    """
+
+    for error_name in ['WSAEWOULDBLOCK', 'EWOULDBLOCK', 'EAGAIN']:
+        if (error_name in dir(errno) and
+            error_number == getattr(errno, error_name)):
+            return True
+    return False
+
+
+def drain_received_data(raw_socket):
+    # Set the socket non-blocking.
+    original_timeout = raw_socket.gettimeout()
+    raw_socket.settimeout(0.0)
+
+    drained_data = []
+
+    # Drain until the socket is closed or no data is immediately
+    # available for read.
+    while True:
+        try:
+            data = raw_socket.recv(1)
+            if not data:
+                break
+            drained_data.append(data)
+        except socket.error, e:
+            # e can be either a pair (errno, string) or just a string (or
+            # something else) telling what went wrong. We suppress only
+            # the errors that indicates that the socket blocks. Those
+            # exceptions can be parsed as a pair (errno, string).
+            try:
+                error_number, message = e
+            except:
+                # Failed to parse socket.error.
+                raise e
+
+            if _is_ewouldblock_errno(error_number):
+                break
+            else:
+                raise e
+
+    # Rollback timeout value.
+    raw_socket.settimeout(original_timeout)
+
+    return ''.join(drained_data)
+
+
+# vi:sts=4 sw=4 et
diff --git a/Tools/Scripts/webkitpy/thirdparty/ordered_dict.py b/Tools/Scripts/webkitpy/thirdparty/ordered_dict.py
new file mode 100644
index 0000000..3dc735a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/thirdparty/ordered_dict.py
@@ -0,0 +1,89 @@
+# Copyright (c) 2009 Raymond Hettinger.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+# This code is obtained from http://code.activestate.com/recipes/576669/
+
+from collections import MutableMapping
+
+class OrderedDict(dict, MutableMapping):
+
+    # Methods with direct access to underlying attributes
+
+    def __init__(self, *args, **kwds):
+        if len(args) > 1:
+            raise TypeError('expected at 1 argument, got %d', len(args))
+        if not hasattr(self, '_keys'):
+            self._keys = []
+        self.update(*args, **kwds)
+
+    def clear(self):
+        del self._keys[:]
+        dict.clear(self)
+
+    def __setitem__(self, key, value):
+        if key not in self:
+            self._keys.append(key)
+        dict.__setitem__(self, key, value)
+
+    def __delitem__(self, key):
+        dict.__delitem__(self, key)
+        self._keys.remove(key)
+
+    def __iter__(self):
+        return iter(self._keys)
+
+    def __reversed__(self):
+        return reversed(self._keys)
+
+    def popitem(self):
+        if not self:
+            raise KeyError
+        key = self._keys.pop()
+        value = dict.pop(self, key)
+        return key, value
+
+    def __reduce__(self):
+        items = [[k, self[k]] for k in self]
+        inst_dict = vars(self).copy()
+        inst_dict.pop('_keys', None)
+        return (self.__class__, (items,), inst_dict)
+
+    # Methods with indirect access via the above methods
+
+    setdefault = MutableMapping.setdefault
+    update = MutableMapping.update
+    pop = MutableMapping.pop
+    keys = MutableMapping.keys
+    values = MutableMapping.values
+    items = MutableMapping.items
+
+    def __repr__(self):
+        pairs = ', '.join(map('%r: %r'.__mod__, self.items()))
+        return '%s({%s})' % (self.__class__.__name__, pairs)
+
+    def copy(self):
+        return self.__class__(self)
+
+    @classmethod
+    def fromkeys(cls, iterable, value=None):
+        d = cls()
+        for key in iterable:
+            d[key] = value
+        return d
diff --git a/Tools/Scripts/webkitpy/to_be_moved/__init__.py b/Tools/Scripts/webkitpy/to_be_moved/__init__.py
new file mode 100644
index 0000000..c0528b7
--- /dev/null
+++ b/Tools/Scripts/webkitpy/to_be_moved/__init__.py
@@ -0,0 +1,10 @@
+# Required for Python to search this directory for module files
+# This directory houses Python modules that do not yet have a proper home.
+#
+# Some of the Python modules in this directory aren't really part of webkitpy
+# in the sense that they're not classes that are meant to be used as part of
+# the webkitpy library. Instead, they're a bunch of helper code for individual
+# scripts in in Tools/Scripts.
+#
+# Really, all this code should either be refactored or moved somewhere else,
+# hence the somewhat lame name for this directory.
diff --git a/Tools/Scripts/webkitpy/to_be_moved/update_webgl_conformance_tests.py b/Tools/Scripts/webkitpy/to_be_moved/update_webgl_conformance_tests.py
new file mode 100755
index 0000000..68c2fb7
--- /dev/null
+++ b/Tools/Scripts/webkitpy/to_be_moved/update_webgl_conformance_tests.py
@@ -0,0 +1,163 @@
+#!/usr/bin/env python
+
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 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 COMPUTER, INC. ``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 COMPUTER, INC. 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 logging
+import optparse
+import os
+import re
+import sys
+from webkitpy.common.checkout import scm
+from webkitpy.common.system.filesystem import FileSystem
+from webkitpy.common.system.executive import Executive
+
+
+_log = logging.getLogger(__name__)
+
+
+def remove_first_line_comment(text):
+    return re.compile(r'^<!--.*?-->\s*', re.DOTALL).sub('', text)
+
+
+def translate_includes(text):
+    # Mapping of single filename to relative path under WebKit root.
+    # Assumption: these filenames are globally unique.
+    include_mapping = {
+        "js-test-style.css": "../../js/resources",
+        "js-test-pre.js": "../../js/resources",
+        "js-test-post.js": "../../js/resources",
+        "desktop-gl-constants.js": "resources",
+    }
+
+    for filename, path in include_mapping.items():
+        search = r'(?:[^"\'= ]*/)?' + re.escape(filename)
+        # We use '/' instead of os.path.join in order to produce consistent
+        # output cross-platform.
+        replace = path + '/' + filename
+        text = re.sub(search, replace, text)
+
+    return text
+
+
+def translate_khronos_test(text):
+    """
+    This method translates the contents of a Khronos test to a WebKit test.
+    """
+
+    translateFuncs = [
+        remove_first_line_comment,
+        translate_includes,
+    ]
+
+    for f in translateFuncs:
+        text = f(text)
+
+    return text
+
+
+def update_file(in_filename, out_dir):
+    # check in_filename exists
+    # check out_dir exists
+    out_filename = os.path.join(out_dir, os.path.basename(in_filename))
+
+    _log.debug("Processing " + in_filename)
+    with open(in_filename, 'r') as in_file:
+        with open(out_filename, 'w') as out_file:
+            out_file.write(translate_khronos_test(in_file.read()))
+
+
+def update_directory(in_dir, out_dir):
+    for filename in glob.glob(os.path.join(in_dir, '*.html')):
+        update_file(os.path.join(in_dir, filename), out_dir)
+
+
+def default_out_dir():
+    detector = scm.SCMDetector(FileSystem(), Executive())
+    current_scm = detector.detect_scm_system(os.path.dirname(sys.argv[0]))
+    if not current_scm:
+        return os.getcwd()
+    root_dir = current_scm.checkout_root
+    if not root_dir:
+        return os.getcwd()
+    out_dir = os.path.join(root_dir, "LayoutTests/fast/canvas/webgl")
+    if os.path.isdir(out_dir):
+        return out_dir
+    return os.getcwd()
+
+
+def configure_logging(options):
+    """Configures the logging system."""
+    log_fmt = '%(levelname)s: %(message)s'
+    log_datefmt = '%y%m%d %H:%M:%S'
+    log_level = logging.INFO
+    if options.verbose:
+        log_fmt = ('%(asctime)s %(filename)s:%(lineno)-4d %(levelname)s '
+                   '%(message)s')
+        log_level = logging.DEBUG
+    logging.basicConfig(level=log_level, format=log_fmt,
+                        datefmt=log_datefmt)
+
+
+def option_parser():
+    usage = "usage: %prog [options] (input file or directory)"
+    parser = optparse.OptionParser(usage=usage)
+    parser.add_option('-v', '--verbose',
+                             action='store_true',
+                             default=False,
+                             help='include debug-level logging')
+    parser.add_option('-o', '--output',
+                             action='store',
+                             type='string',
+                             default=default_out_dir(),
+                             metavar='DIR',
+                             help='specify an output directory to place files '
+                                  'in [default: %default]')
+    return parser
+
+
+def main():
+    parser = option_parser()
+    (options, args) = parser.parse_args()
+    configure_logging(options)
+
+    if len(args) == 0:
+        _log.error("Must specify an input directory or filename.")
+        parser.print_help()
+        return 1
+
+    in_name = args[0]
+    if os.path.isfile(in_name):
+        update_file(in_name, options.output)
+    elif os.path.isdir(in_name):
+        update_directory(in_name, options.output)
+    else:
+        _log.error("'%s' is not a directory or a file.", in_name)
+        return 2
+
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/Tools/Scripts/webkitpy/to_be_moved/update_webgl_conformance_tests_unittest.py b/Tools/Scripts/webkitpy/to_be_moved/update_webgl_conformance_tests_unittest.py
new file mode 100644
index 0000000..b3b4d58
--- /dev/null
+++ b/Tools/Scripts/webkitpy/to_be_moved/update_webgl_conformance_tests_unittest.py
@@ -0,0 +1,102 @@
+#!/usr/bin/python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Unit tests for update_webgl_conformance_tests."""
+
+import unittest
+from webkitpy.to_be_moved import update_webgl_conformance_tests as webgl
+
+
+def construct_script(name):
+    return "<script src=\"" + name + "\"></script>\n"
+
+
+def construct_style(name):
+    return "<link rel=\"stylesheet\" href=\"" + name + "\">"
+
+
+class TestTranslation(unittest.TestCase):
+    def assert_unchanged(self, text):
+        self.assertEqual(text, webgl.translate_khronos_test(text))
+
+    def assert_translate(self, input, output):
+        self.assertEqual(output, webgl.translate_khronos_test(input))
+
+    def test_simple_unchanged(self):
+        self.assert_unchanged("")
+        self.assert_unchanged("<html></html>")
+
+    def test_header_strip(self):
+        single_line_header = "<!-- single line header. -->"
+        multi_line_header = """<!-- this is a multi-line
+                header.  it should all be removed too.
+                -->"""
+        text = "<html></html>"
+        self.assert_translate(single_line_header, "")
+        self.assert_translate(single_line_header + text, text)
+        self.assert_translate(multi_line_header + text, text)
+
+    def dont_strip_other_headers(self):
+        self.assert_unchanged("<html>\n<!-- don't remove comments on other lines. -->\n</html>")
+
+    def test_include_rewriting(self):
+        # Mappings to None are unchanged
+        styles = {
+            "../resources/js-test-style.css": "../../js/resources/js-test-style.css",
+            "fail.css": None,
+            "resources/stylesheet.css": None,
+            "../resources/style.css": None,
+        }
+        scripts = {
+            "../resources/js-test-pre.js": "../../js/resources/js-test-pre.js",
+            "../resources/js-test-post.js": "../../js/resources/js-test-post.js",
+            "../resources/desktop-gl-constants.js": "resources/desktop-gl-constants.js",
+
+            "resources/shadow-offset.js": None,
+            "../resources/js-test-post-async.js": None,
+        }
+
+        input_text = ""
+        output_text = ""
+        for input, output in styles.items():
+            input_text += construct_style(input)
+            output_text += construct_style(output if output else input)
+        for input, output in scripts.items():
+            input_text += construct_script(input)
+            output_text += construct_script(output if output else input)
+
+        head = '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">\n<html>\n<head>\n'
+        foot = '</head>\n<body>\n</body>\n</html>'
+        input_text = head + input_text + foot
+        output_text = head + output_text + foot
+        self.assert_translate(input_text, output_text)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/tool/__init__.py b/Tools/Scripts/webkitpy/tool/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/tool/bot/__init__.py b/Tools/Scripts/webkitpy/tool/bot/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/tool/bot/botinfo.py b/Tools/Scripts/webkitpy/tool/bot/botinfo.py
new file mode 100644
index 0000000..b9fd938
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/botinfo.py
@@ -0,0 +1,39 @@
+# Copyright (c) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+# FIXME: We should consider hanging one of these off the tool object.
+class BotInfo(object):
+    def __init__(self, tool):
+        self._tool = tool
+
+    def summary_text(self):
+        # bot_id is also stored on the options dictionary on the tool.
+        bot_id = self._tool.status_server.bot_id
+        bot_id_string = "Bot: %s  " % (bot_id) if bot_id else ""
+        return "%sPort: %s  Platform: %s" % (bot_id_string, self._tool.port().name(), self._tool.platform.display_name())
diff --git a/Tools/Scripts/webkitpy/tool/bot/botinfo_unittest.py b/Tools/Scripts/webkitpy/tool/bot/botinfo_unittest.py
new file mode 100644
index 0000000..820ff55
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/botinfo_unittest.py
@@ -0,0 +1,41 @@
+# Copyright (c) 2011 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.tool.bot.botinfo import BotInfo
+from webkitpy.tool.mocktool import MockTool
+from webkitpy.common.net.statusserver_mock import MockStatusServer
+
+
+class BotInfoTest(unittest.TestCase):
+
+    def test_summary_text(self):
+        tool = MockTool()
+        tool.status_server = MockStatusServer("MockBotId")
+        self.assertEqual(BotInfo(tool).summary_text(), "Bot: MockBotId  Port: MockPort  Platform: MockPlatform 1.0")
diff --git a/Tools/Scripts/webkitpy/tool/bot/commitqueuetask.py b/Tools/Scripts/webkitpy/tool/bot/commitqueuetask.py
new file mode 100644
index 0000000..491ba79
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/commitqueuetask.py
@@ -0,0 +1,94 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.bot.patchanalysistask import PatchAnalysisTask, PatchAnalysisTaskDelegate
+
+
+class CommitQueueTaskDelegate(PatchAnalysisTaskDelegate):
+    def parent_command(self):
+        return "commit-queue"
+
+    def did_pass_testing_ews(self, patch):
+        raise NotImplementedError("subclasses must implement")
+
+
+class CommitQueueTask(PatchAnalysisTask):
+    def validate(self):
+        # Bugs might get closed, or patches might be obsoleted or r-'d while the
+        # commit-queue is processing.
+        self._patch = self._delegate.refetch_patch(self._patch)
+        if self._patch.is_obsolete():
+            return False
+        if self._patch.bug().is_closed():
+            return False
+        if not self._patch.committer():
+            return False
+        if self._patch.review() == "-":
+            return False
+        return True
+
+    def _validate_changelog(self):
+        return self._run_command([
+            "validate-changelog",
+            "--non-interactive",
+            self._patch.id(),
+        ],
+        "ChangeLog validated",
+        "ChangeLog did not pass validation")
+
+    def _did_pass_tests_recently(self):
+        if self._delegate.did_pass_testing_ews(self._patch):
+            return True
+        return self._test_patch()
+
+    def run(self):
+        if not self.validate():
+            return False
+        if not self._clean():
+            return False
+        if not self._update():
+            return False
+        if not self._apply():
+            return self.report_failure()
+        if not self._validate_changelog():
+            return self.report_failure()
+        if not self._patch.is_rollout():
+            if not self._build():
+                if not self._build_without_patch():
+                    return False
+                return self.report_failure()
+            if not self._did_pass_tests_recently():
+                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():
+            return False
+        # FIXME: We should understand why the land failure occured and retry if possible.
+        if not self._land():
+            return self.report_failure()
+        return True
diff --git a/Tools/Scripts/webkitpy/tool/bot/commitqueuetask_unittest.py b/Tools/Scripts/webkitpy/tool/bot/commitqueuetask_unittest.py
new file mode 100644
index 0000000..8b33416
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/commitqueuetask_unittest.py
@@ -0,0 +1,580 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from datetime import datetime
+import unittest
+
+from webkitpy.common.net import bugzilla
+from webkitpy.common.net.layouttestresults import LayoutTestResults
+from webkitpy.common.system.deprecated_logging import error, log
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.layout_tests.models import test_results
+from webkitpy.layout_tests.models import test_failures
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.bot.commitqueuetask import *
+from webkitpy.tool.bot.expectedfailures import ExpectedFailures
+from webkitpy.tool.mocktool import MockTool
+
+
+class MockCommitQueue(CommitQueueTaskDelegate):
+    def __init__(self, error_plan):
+        self._error_plan = error_plan
+        self._failure_status_id = 0
+
+    def run_command(self, command):
+        log("run_webkit_patch: %s" % command)
+        if self._error_plan:
+            error = self._error_plan.pop(0)
+            if error:
+                raise error
+
+    def command_passed(self, success_message, patch):
+        log("command_passed: success_message='%s' patch='%s'" % (
+            success_message, patch.id()))
+
+    def command_failed(self, failure_message, script_error, patch):
+        log("command_failed: failure_message='%s' script_error='%s' patch='%s'" % (
+            failure_message, script_error, patch.id()))
+        self._failure_status_id += 1
+        return self._failure_status_id
+
+    def refetch_patch(self, patch):
+        return patch
+
+    def expected_failures(self):
+        return ExpectedFailures()
+
+    def test_results(self):
+        return None
+
+    def report_flaky_tests(self, patch, flaky_results, results_archive):
+        flaky_tests = [result.filename for result in flaky_results]
+        log("report_flaky_tests: patch='%s' flaky_tests='%s' archive='%s'" % (patch.id(), flaky_tests, results_archive.filename))
+
+    def archive_last_test_results(self, patch):
+        log("archive_last_test_results: patch='%s'" % patch.id())
+        archive = Mock()
+        archive.filename = "mock-archive-%s.zip" % patch.id()
+        return archive
+
+    def build_style(self):
+        return "both"
+
+    def did_pass_testing_ews(self, patch):
+        return False
+
+
+class FailingTestCommitQueue(MockCommitQueue):
+    def __init__(self, error_plan, test_failure_plan):
+        MockCommitQueue.__init__(self, error_plan)
+        self._test_run_counter = -1  # Special value to indicate tests have never been run.
+        self._test_failure_plan = test_failure_plan
+
+    def run_command(self, command):
+        if command[0] == "build-and-test":
+            self._test_run_counter += 1
+        MockCommitQueue.run_command(self, command)
+
+    def _mock_test_result(self, testname):
+        return test_results.TestResult(testname, [test_failures.FailureTextMismatch()])
+
+    def test_results(self):
+        # Doesn't make sense to ask for the test_results until the tests have run at least once.
+        assert(self._test_run_counter >= 0)
+        failures_for_run = self._test_failure_plan[self._test_run_counter]
+        results = LayoutTestResults(map(self._mock_test_result, failures_for_run))
+        # This makes the results trustable by ExpectedFailures.
+        results.set_failure_limit_count(10)
+        return results
+
+
+# We use GoldenScriptError to make sure that the code under test throws the
+# correct (i.e., golden) exception.
+class GoldenScriptError(ScriptError):
+    pass
+
+
+class CommitQueueTaskTest(unittest.TestCase):
+    def _run_through_task(self, commit_queue, expected_stderr, expected_exception=None, expect_retry=False):
+        tool = MockTool(log_executive=True)
+        patch = tool.bugs.fetch_attachment(10000)
+        task = CommitQueueTask(commit_queue, patch)
+        success = OutputCapture().assert_outputs(self, task.run, expected_stderr=expected_stderr, expected_exception=expected_exception)
+        if not expected_exception:
+            self.assertEqual(success, not expect_retry)
+        return task
+
+    def test_success_case(self):
+        commit_queue = MockCommitQueue([])
+        expected_stderr = """run_webkit_patch: ['clean']
+command_passed: success_message='Cleaned working directory' patch='10000'
+run_webkit_patch: ['update']
+command_passed: success_message='Updated working directory' patch='10000'
+run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000]
+command_passed: success_message='Applied patch' patch='10000'
+run_webkit_patch: ['validate-changelog', '--non-interactive', 10000]
+command_passed: success_message='ChangeLog validated' patch='10000'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_passed: success_message='Built patch' patch='10000'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_passed: success_message='Passed tests' patch='10000'
+run_webkit_patch: ['land-attachment', '--force-clean', '--non-interactive', '--parent-command=commit-queue', 10000]
+command_passed: success_message='Landed patch' patch='10000'
+"""
+        self._run_through_task(commit_queue, expected_stderr)
+
+    def test_fast_success_case(self):
+        commit_queue = MockCommitQueue([])
+        commit_queue.did_pass_testing_ews = lambda patch: True
+        expected_stderr = """run_webkit_patch: ['clean']
+command_passed: success_message='Cleaned working directory' patch='10000'
+run_webkit_patch: ['update']
+command_passed: success_message='Updated working directory' patch='10000'
+run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000]
+command_passed: success_message='Applied patch' patch='10000'
+run_webkit_patch: ['validate-changelog', '--non-interactive', 10000]
+command_passed: success_message='ChangeLog validated' patch='10000'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_passed: success_message='Built patch' patch='10000'
+run_webkit_patch: ['land-attachment', '--force-clean', '--non-interactive', '--parent-command=commit-queue', 10000]
+command_passed: success_message='Landed patch' patch='10000'
+"""
+        self._run_through_task(commit_queue, expected_stderr)
+
+    def test_clean_failure(self):
+        commit_queue = MockCommitQueue([
+            ScriptError("MOCK clean failure"),
+        ])
+        expected_stderr = """run_webkit_patch: ['clean']
+command_failed: failure_message='Unable to clean working directory' script_error='MOCK clean failure' patch='10000'
+"""
+        self._run_through_task(commit_queue, expected_stderr, expect_retry=True)
+
+    def test_update_failure(self):
+        commit_queue = MockCommitQueue([
+            None,
+            ScriptError("MOCK update failure"),
+        ])
+        expected_stderr = """run_webkit_patch: ['clean']
+command_passed: success_message='Cleaned working directory' patch='10000'
+run_webkit_patch: ['update']
+command_failed: failure_message='Unable to update working directory' script_error='MOCK update failure' patch='10000'
+"""
+        self._run_through_task(commit_queue, expected_stderr, expect_retry=True)
+
+    def test_apply_failure(self):
+        commit_queue = MockCommitQueue([
+            None,
+            None,
+            GoldenScriptError("MOCK apply failure"),
+        ])
+        expected_stderr = """run_webkit_patch: ['clean']
+command_passed: success_message='Cleaned working directory' patch='10000'
+run_webkit_patch: ['update']
+command_passed: success_message='Updated working directory' patch='10000'
+run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000]
+command_failed: failure_message='Patch does not apply' script_error='MOCK apply failure' patch='10000'
+"""
+        self._run_through_task(commit_queue, expected_stderr, GoldenScriptError)
+
+    def test_validate_changelog_failure(self):
+        commit_queue = MockCommitQueue([
+            None,
+            None,
+            None,
+            GoldenScriptError("MOCK validate failure"),
+        ])
+        expected_stderr = """run_webkit_patch: ['clean']
+command_passed: success_message='Cleaned working directory' patch='10000'
+run_webkit_patch: ['update']
+command_passed: success_message='Updated working directory' patch='10000'
+run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000]
+command_passed: success_message='Applied patch' patch='10000'
+run_webkit_patch: ['validate-changelog', '--non-interactive', 10000]
+command_failed: failure_message='ChangeLog did not pass validation' script_error='MOCK validate failure' patch='10000'
+"""
+        self._run_through_task(commit_queue, expected_stderr, GoldenScriptError)
+
+    def test_build_failure(self):
+        commit_queue = MockCommitQueue([
+            None,
+            None,
+            None,
+            None,
+            GoldenScriptError("MOCK build failure"),
+        ])
+        expected_stderr = """run_webkit_patch: ['clean']
+command_passed: success_message='Cleaned working directory' patch='10000'
+run_webkit_patch: ['update']
+command_passed: success_message='Updated working directory' patch='10000'
+run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000]
+command_passed: success_message='Applied patch' patch='10000'
+run_webkit_patch: ['validate-changelog', '--non-interactive', 10000]
+command_passed: success_message='ChangeLog validated' patch='10000'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_failed: failure_message='Patch does not build' script_error='MOCK build failure' patch='10000'
+run_webkit_patch: ['build', '--force-clean', '--no-update', '--build-style=both']
+command_passed: success_message='Able to build without patch' patch='10000'
+"""
+        self._run_through_task(commit_queue, expected_stderr, GoldenScriptError)
+
+    def test_red_build_failure(self):
+        commit_queue = MockCommitQueue([
+            None,
+            None,
+            None,
+            None,
+            ScriptError("MOCK build failure"),
+            ScriptError("MOCK clean build failure"),
+        ])
+        expected_stderr = """run_webkit_patch: ['clean']
+command_passed: success_message='Cleaned working directory' patch='10000'
+run_webkit_patch: ['update']
+command_passed: success_message='Updated working directory' patch='10000'
+run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000]
+command_passed: success_message='Applied patch' patch='10000'
+run_webkit_patch: ['validate-changelog', '--non-interactive', 10000]
+command_passed: success_message='ChangeLog validated' patch='10000'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_failed: failure_message='Patch does not build' script_error='MOCK build failure' patch='10000'
+run_webkit_patch: ['build', '--force-clean', '--no-update', '--build-style=both']
+command_failed: failure_message='Unable to build without patch' script_error='MOCK clean build failure' patch='10000'
+"""
+        self._run_through_task(commit_queue, expected_stderr, expect_retry=True)
+
+    def test_flaky_test_failure(self):
+        commit_queue = MockCommitQueue([
+            None,
+            None,
+            None,
+            None,
+            None,
+            ScriptError("MOCK tests failure"),
+        ])
+        # CommitQueueTask will only report flaky tests if we successfully parsed
+        # results.html and returned a LayoutTestResults object, so we fake one.
+        commit_queue.test_results = lambda: LayoutTestResults([])
+        expected_stderr = """run_webkit_patch: ['clean']
+command_passed: success_message='Cleaned working directory' patch='10000'
+run_webkit_patch: ['update']
+command_passed: success_message='Updated working directory' patch='10000'
+run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000]
+command_passed: success_message='Applied patch' patch='10000'
+run_webkit_patch: ['validate-changelog', '--non-interactive', 10000]
+command_passed: success_message='ChangeLog validated' patch='10000'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_passed: success_message='Built patch' patch='10000'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_failed: failure_message='Patch does not pass tests' script_error='MOCK tests failure' patch='10000'
+archive_last_test_results: patch='10000'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_passed: success_message='Passed tests' patch='10000'
+report_flaky_tests: patch='10000' flaky_tests='[]' archive='mock-archive-10000.zip'
+run_webkit_patch: ['land-attachment', '--force-clean', '--non-interactive', '--parent-command=commit-queue', 10000]
+command_passed: success_message='Landed patch' patch='10000'
+"""
+        self._run_through_task(commit_queue, expected_stderr)
+
+    def test_failed_archive(self):
+        commit_queue = MockCommitQueue([
+            None,
+            None,
+            None,
+            None,
+            None,
+            ScriptError("MOCK tests failure"),
+        ])
+        commit_queue.test_results = lambda: LayoutTestResults([])
+        # It's possible delegate to fail to archive layout tests, don't try to report
+        # flaky tests when that happens.
+        commit_queue.archive_last_test_results = lambda patch: None
+        expected_stderr = """run_webkit_patch: ['clean']
+command_passed: success_message='Cleaned working directory' patch='10000'
+run_webkit_patch: ['update']
+command_passed: success_message='Updated working directory' patch='10000'
+run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000]
+command_passed: success_message='Applied patch' patch='10000'
+run_webkit_patch: ['validate-changelog', '--non-interactive', 10000]
+command_passed: success_message='ChangeLog validated' patch='10000'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_passed: success_message='Built patch' patch='10000'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_failed: failure_message='Patch does not pass tests' script_error='MOCK tests failure' patch='10000'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_passed: success_message='Passed tests' patch='10000'
+run_webkit_patch: ['land-attachment', '--force-clean', '--non-interactive', '--parent-command=commit-queue', 10000]
+command_passed: success_message='Landed patch' patch='10000'
+"""
+        self._run_through_task(commit_queue, expected_stderr)
+
+    def test_double_flaky_test_failure(self):
+        commit_queue = FailingTestCommitQueue([
+            None,
+            None,
+            None,
+            None,
+            None,
+            ScriptError("MOCK test failure"),
+            ScriptError("MOCK test failure again"),
+        ], [
+            "foo.html",
+            "bar.html",
+            "foo.html",
+        ])
+        # The (subtle) point of this test is that report_flaky_tests does not appear
+        # in the expected_stderr for this run.
+        # Note also that there is no attempt to run the tests w/o the patch.
+        expected_stderr = """run_webkit_patch: ['clean']
+command_passed: success_message='Cleaned working directory' patch='10000'
+run_webkit_patch: ['update']
+command_passed: success_message='Updated working directory' patch='10000'
+run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000]
+command_passed: success_message='Applied patch' patch='10000'
+run_webkit_patch: ['validate-changelog', '--non-interactive', 10000]
+command_passed: success_message='ChangeLog validated' patch='10000'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_passed: success_message='Built patch' patch='10000'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure' patch='10000'
+archive_last_test_results: patch='10000'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure again' patch='10000'
+"""
+        tool = MockTool(log_executive=True)
+        patch = tool.bugs.fetch_attachment(10000)
+        task = CommitQueueTask(commit_queue, patch)
+        success = OutputCapture().assert_outputs(self, task.run, expected_stderr=expected_stderr)
+        self.assertEqual(success, False)
+
+    def test_test_failure(self):
+        commit_queue = MockCommitQueue([
+            None,
+            None,
+            None,
+            None,
+            None,
+            GoldenScriptError("MOCK test failure"),
+            ScriptError("MOCK test failure again"),
+        ])
+        expected_stderr = """run_webkit_patch: ['clean']
+command_passed: success_message='Cleaned working directory' patch='10000'
+run_webkit_patch: ['update']
+command_passed: success_message='Updated working directory' patch='10000'
+run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000]
+command_passed: success_message='Applied patch' patch='10000'
+run_webkit_patch: ['validate-changelog', '--non-interactive', 10000]
+command_passed: success_message='ChangeLog validated' patch='10000'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_passed: success_message='Built patch' patch='10000'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure' patch='10000'
+archive_last_test_results: patch='10000'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure again' patch='10000'
+archive_last_test_results: patch='10000'
+run_webkit_patch: ['build-and-test', '--force-clean', '--no-update', '--build', '--test', '--non-interactive']
+command_passed: success_message='Able to pass tests without patch' patch='10000'
+"""
+        self._run_through_task(commit_queue, expected_stderr, GoldenScriptError)
+
+    def test_red_test_failure(self):
+        commit_queue = FailingTestCommitQueue([
+            None,
+            None,
+            None,
+            None,
+            None,
+            ScriptError("MOCK test failure"),
+            ScriptError("MOCK test failure again"),
+            ScriptError("MOCK clean test failure"),
+        ], [
+            "foo.html",
+            "foo.html",
+            "foo.html",
+        ])
+
+        # Tests always fail, and always return the same results, but we
+        # should still be able to land in this case!
+        expected_stderr = """run_webkit_patch: ['clean']
+command_passed: success_message='Cleaned working directory' patch='10000'
+run_webkit_patch: ['update']
+command_passed: success_message='Updated working directory' patch='10000'
+run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000]
+command_passed: success_message='Applied patch' patch='10000'
+run_webkit_patch: ['validate-changelog', '--non-interactive', 10000]
+command_passed: success_message='ChangeLog validated' patch='10000'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_passed: success_message='Built patch' patch='10000'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure' patch='10000'
+archive_last_test_results: patch='10000'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure again' patch='10000'
+archive_last_test_results: patch='10000'
+run_webkit_patch: ['build-and-test', '--force-clean', '--no-update', '--build', '--test', '--non-interactive']
+command_failed: failure_message='Unable to pass tests without patch (tree is red?)' script_error='MOCK clean test failure' patch='10000'
+run_webkit_patch: ['land-attachment', '--force-clean', '--non-interactive', '--parent-command=commit-queue', 10000]
+command_passed: success_message='Landed patch' patch='10000'
+"""
+        self._run_through_task(commit_queue, expected_stderr)
+
+    def test_very_red_tree_retry(self):
+        lots_of_failing_tests = map(lambda num: "test-%s.html" % num, range(0, 100))
+        commit_queue = FailingTestCommitQueue([
+            None,
+            None,
+            None,
+            None,
+            None,
+            ScriptError("MOCK test failure"),
+            ScriptError("MOCK test failure again"),
+            ScriptError("MOCK clean test failure"),
+        ], [
+            lots_of_failing_tests,
+            lots_of_failing_tests,
+            lots_of_failing_tests,
+        ])
+
+        # Tests always fail, and return so many failures that we do not
+        # trust the results (see ExpectedFailures._can_trust_results) so we
+        # just give up and retry the patch.
+        expected_stderr = """run_webkit_patch: ['clean']
+command_passed: success_message='Cleaned working directory' patch='10000'
+run_webkit_patch: ['update']
+command_passed: success_message='Updated working directory' patch='10000'
+run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000]
+command_passed: success_message='Applied patch' patch='10000'
+run_webkit_patch: ['validate-changelog', '--non-interactive', 10000]
+command_passed: success_message='ChangeLog validated' patch='10000'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_passed: success_message='Built patch' patch='10000'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure' patch='10000'
+archive_last_test_results: patch='10000'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure again' patch='10000'
+archive_last_test_results: patch='10000'
+run_webkit_patch: ['build-and-test', '--force-clean', '--no-update', '--build', '--test', '--non-interactive']
+command_failed: failure_message='Unable to pass tests without patch (tree is red?)' script_error='MOCK clean test failure' patch='10000'
+"""
+        self._run_through_task(commit_queue, expected_stderr, expect_retry=True)
+
+    def test_red_tree_patch_rejection(self):
+        commit_queue = FailingTestCommitQueue([
+            None,
+            None,
+            None,
+            None,
+            None,
+            GoldenScriptError("MOCK test failure"),
+            ScriptError("MOCK test failure again"),
+            ScriptError("MOCK clean test failure"),
+        ], [
+            ["foo.html", "bar.html"],
+            ["foo.html", "bar.html"],
+            ["foo.html"],
+        ])
+
+        # Tests always fail, but the clean tree only fails one test
+        # while the patch fails two.  So we should reject the patch!
+        expected_stderr = """run_webkit_patch: ['clean']
+command_passed: success_message='Cleaned working directory' patch='10000'
+run_webkit_patch: ['update']
+command_passed: success_message='Updated working directory' patch='10000'
+run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000]
+command_passed: success_message='Applied patch' patch='10000'
+run_webkit_patch: ['validate-changelog', '--non-interactive', 10000]
+command_passed: success_message='ChangeLog validated' patch='10000'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_passed: success_message='Built patch' patch='10000'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure' patch='10000'
+archive_last_test_results: patch='10000'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure again' patch='10000'
+archive_last_test_results: patch='10000'
+run_webkit_patch: ['build-and-test', '--force-clean', '--no-update', '--build', '--test', '--non-interactive']
+command_failed: failure_message='Unable to pass tests without patch (tree is red?)' script_error='MOCK clean test failure' patch='10000'
+"""
+        task = self._run_through_task(commit_queue, expected_stderr, GoldenScriptError)
+        self.assertEqual(task.results_from_patch_test_run(task._patch).failing_tests(), ["foo.html", "bar.html"])
+        # failure_status_id should be of the test with patch (1), not the test without patch (2).
+        self.assertEqual(task.failure_status_id, 1)
+
+    def test_land_failure(self):
+        commit_queue = MockCommitQueue([
+            None,
+            None,
+            None,
+            None,
+            None,
+            None,
+            GoldenScriptError("MOCK land failure"),
+        ])
+        expected_stderr = """run_webkit_patch: ['clean']
+command_passed: success_message='Cleaned working directory' patch='10000'
+run_webkit_patch: ['update']
+command_passed: success_message='Updated working directory' patch='10000'
+run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000]
+command_passed: success_message='Applied patch' patch='10000'
+run_webkit_patch: ['validate-changelog', '--non-interactive', 10000]
+command_passed: success_message='ChangeLog validated' patch='10000'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_passed: success_message='Built patch' patch='10000'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_passed: success_message='Passed tests' patch='10000'
+run_webkit_patch: ['land-attachment', '--force-clean', '--non-interactive', '--parent-command=commit-queue', 10000]
+command_failed: failure_message='Unable to land patch' script_error='MOCK land failure' patch='10000'
+"""
+        # FIXME: This should really be expect_retry=True for a better user experiance.
+        self._run_through_task(commit_queue, expected_stderr, GoldenScriptError)
+
+    def _expect_validate(self, patch, is_valid):
+        class MockDelegate(object):
+            def refetch_patch(self, patch):
+                return patch
+
+            def expected_failures(self):
+                return ExpectedFailures()
+
+        task = CommitQueueTask(MockDelegate(), patch)
+        self.assertEquals(task.validate(), is_valid)
+
+    def _mock_patch(self, attachment_dict={}, bug_dict={'bug_status': 'NEW'}, committer="fake"):
+        bug = bugzilla.Bug(bug_dict, None)
+        patch = bugzilla.Attachment(attachment_dict, bug)
+        patch._committer = committer
+        return patch
+
+    def test_validate(self):
+        self._expect_validate(self._mock_patch(), True)
+        self._expect_validate(self._mock_patch({'is_obsolete': True}), False)
+        self._expect_validate(self._mock_patch(bug_dict={'bug_status': 'CLOSED'}), False)
+        self._expect_validate(self._mock_patch(committer=None), False)
+        self._expect_validate(self._mock_patch({'review': '-'}), False)
diff --git a/Tools/Scripts/webkitpy/tool/bot/earlywarningsystemtask.py b/Tools/Scripts/webkitpy/tool/bot/earlywarningsystemtask.py
new file mode 100644
index 0000000..b66cfbc
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/earlywarningsystemtask.py
@@ -0,0 +1,66 @@
+# Copyright (c) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.bot.patchanalysistask import PatchAnalysisTask, PatchAnalysisTaskDelegate, UnableToApplyPatch
+
+
+class EarlyWarningSystemTaskDelegate(PatchAnalysisTaskDelegate):
+    pass
+
+
+class EarlyWarningSystemTask(PatchAnalysisTask):
+    def __init__(self, delegate, patch, should_run_tests=True):
+        PatchAnalysisTask.__init__(self, delegate, patch)
+        self._should_run_tests = should_run_tests
+
+    def validate(self):
+        self._patch = self._delegate.refetch_patch(self._patch)
+        if self._patch.is_obsolete():
+            return False
+        if self._patch.bug().is_closed():
+            return False
+        if self._patch.review() == "-":
+            return False
+        return True
+
+    def run(self):
+        if not self.validate():
+            return False
+        if not self._clean():
+            return False
+        if not self._update():
+            return False
+        if not self._apply():
+            raise UnableToApplyPatch(self._patch)
+        if not self._build():
+            if not self._build_without_patch():
+                return False
+            return self.report_failure()
+        if not self._should_run_tests:
+            return True
+        return self._test_patch()
diff --git a/Tools/Scripts/webkitpy/tool/bot/expectedfailures.py b/Tools/Scripts/webkitpy/tool/bot/expectedfailures.py
new file mode 100644
index 0000000..c0cfe21
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/expectedfailures.py
@@ -0,0 +1,60 @@
+# Copyright (c) 2011 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 ExpectedFailures(object):
+    def __init__(self):
+        self._failures = set()
+        self._is_trustworthy = True
+
+    @classmethod
+    def _has_failures(cls, results):
+        return bool(results and results.failing_tests())
+
+    @classmethod
+    def _should_trust(cls, results):
+        return bool(cls._has_failures(results) and results.failure_limit_count() and len(results.failing_tests()) < results.failure_limit_count())
+
+    def failures_were_expected(self, results):
+        if not self._is_trustworthy:
+            return False
+        if not self._should_trust(results):
+            return False
+        return set(results.failing_tests()) <= self._failures
+
+    def unexpected_failures_observed(self, results):
+        if not self._is_trustworthy:
+            return None
+        if not self._has_failures(results):
+            return None
+        return set(results.failing_tests()) - self._failures
+
+    def update(self, results):
+        if results:
+            self._failures = set(results.failing_tests())
+            self._is_trustworthy = self._should_trust(results)
diff --git a/Tools/Scripts/webkitpy/tool/bot/expectedfailures_unittest.py b/Tools/Scripts/webkitpy/tool/bot/expectedfailures_unittest.py
new file mode 100644
index 0000000..4c1c3d9
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/expectedfailures_unittest.py
@@ -0,0 +1,95 @@
+# Copyright (c) 2009 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.tool.bot.expectedfailures import ExpectedFailures
+
+
+class MockResults(object):
+    def __init__(self, failing_tests=[], failure_limit=10):
+        self._failing_tests = failing_tests
+        self._failure_limit_count = failure_limit
+
+    def failure_limit_count(self):
+        return self._failure_limit_count
+
+    def failing_tests(self):
+        return self._failing_tests
+
+
+class ExpectedFailuresTest(unittest.TestCase):
+    def _assert_can_trust(self, results, can_trust):
+        self.assertEquals(ExpectedFailures._should_trust(results), can_trust)
+
+    def test_can_trust_results(self):
+        self._assert_can_trust(None, False)
+        self._assert_can_trust(MockResults(failing_tests=[], failure_limit=None), False)
+        self._assert_can_trust(MockResults(failing_tests=[], failure_limit=10), False)
+        self._assert_can_trust(MockResults(failing_tests=[1], failure_limit=None), False)
+        self._assert_can_trust(MockResults(failing_tests=[1], failure_limit=2), True)
+        self._assert_can_trust(MockResults(failing_tests=[1], failure_limit=1), False)
+        self._assert_can_trust(MockResults(failing_tests=[1, 2], failure_limit=1), False)
+
+    def _assert_expected(self, expected_failures, failures, expected):
+        self.assertEqual(expected_failures.failures_were_expected(MockResults(failures)), expected)
+
+    def test_failures_were_expected(self):
+        failures = ExpectedFailures()
+        failures.update(MockResults(['foo.html']))
+        self._assert_expected(failures, ['foo.html'], True)
+        self._assert_expected(failures, ['bar.html'], False)
+        self._assert_expected(failures, ['bar.html', 'foo.html'], False)
+
+        failures.update(MockResults(['baz.html']))
+        self._assert_expected(failures, ['baz.html'], True)
+        self._assert_expected(failures, ['foo.html'], False)
+
+        failures.update(MockResults([]))
+        self._assert_expected(failures, ['baz.html'], False)
+        self._assert_expected(failures, ['foo.html'], False)
+
+    def test_unexpected_failures_observed(self):
+        failures = ExpectedFailures()
+        failures.update(MockResults(['foo.html']))
+        self.assertEquals(failures.unexpected_failures_observed(MockResults(['foo.html', 'bar.html'])), set(['bar.html']))
+        self.assertEquals(failures.unexpected_failures_observed(MockResults(['baz.html'])), set(['baz.html']))
+        unbounded_results = MockResults(['baz.html', 'qux.html', 'taco.html'], failure_limit=3)
+        self.assertEquals(failures.unexpected_failures_observed(unbounded_results), set(['baz.html', 'qux.html', 'taco.html']))
+        unbounded_results_with_existing_failure = MockResults(['foo.html', 'baz.html', 'qux.html', 'taco.html'], failure_limit=4)
+        self.assertEquals(failures.unexpected_failures_observed(unbounded_results_with_existing_failure), set(['baz.html', 'qux.html', 'taco.html']))
+
+    def test_unexpected_failures_observed_when_tree_is_hosed(self):
+        failures = ExpectedFailures()
+        failures.update(MockResults(['foo.html', 'banana.html'], failure_limit=2))
+        self.assertEquals(failures.unexpected_failures_observed(MockResults(['foo.html', 'bar.html'])), None)
+        self.assertEquals(failures.unexpected_failures_observed(MockResults(['baz.html'])), None)
+        unbounded_results = MockResults(['baz.html', 'qux.html', 'taco.html'], failure_limit=3)
+        self.assertEquals(failures.unexpected_failures_observed(unbounded_results), None)
+        unbounded_results_with_existing_failure = MockResults(['foo.html', 'baz.html', 'qux.html', 'taco.html'], failure_limit=4)
+        self.assertEquals(failures.unexpected_failures_observed(unbounded_results_with_existing_failure), None)
diff --git a/Tools/Scripts/webkitpy/tool/bot/feeders.py b/Tools/Scripts/webkitpy/tool/bot/feeders.py
new file mode 100644
index 0000000..4ba2f04
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/feeders.py
@@ -0,0 +1,95 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.config.committervalidator import CommitterValidator
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.tool.grammar import pluralize
+
+
+class AbstractFeeder(object):
+    def __init__(self, tool):
+        self._tool = tool
+
+    def feed(self):
+        raise NotImplementedError("subclasses must implement")
+
+
+class CommitQueueFeeder(AbstractFeeder):
+    queue_name = "commit-queue"
+
+    def __init__(self, tool):
+        AbstractFeeder.__init__(self, tool)
+        self.committer_validator = CommitterValidator(self._tool)
+
+    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 = self._patches_with_acceptable_review_flag(patches)
+        patches = sorted(patches, self._patch_cmp)
+        patch_ids = [patch.id() for patch in patches]
+        self._update_work_items(patch_ids)
+
+    def _patches_for_bug(self, bug_id):
+        return self._tool.bugs.fetch_bug(bug_id).commit_queued_patches(include_invalid=True)
+
+    # Filters out patches with r? or r-, only r+ or no review are OK to land.
+    def _patches_with_acceptable_review_flag(self, patches):
+        return [patch for patch in patches if patch.review() in [None, '+']]
+
+    def _validate_patches(self):
+        # Not using BugzillaQueries.fetch_patches_from_commit_queue() so we can reject patches with invalid committers/reviewers.
+        bug_ids = self._tool.bugs.queries.fetch_bug_ids_from_commit_queue()
+        all_patches = sum([self._patches_for_bug(bug_id) for bug_id in bug_ids], [])
+        return self.committer_validator.patches_after_rejecting_invalid_commiters_and_reviewers(all_patches)
+
+    def _patch_cmp(self, a, b):
+        # Sort first by is_rollout, then by attach_date.
+        # Reversing the order so that is_rollout is first.
+        rollout_cmp = cmp(b.is_rollout(), a.is_rollout())
+        if rollout_cmp != 0:
+            return rollout_cmp
+        return cmp(a.attach_date(), b.attach_date())
+
+
+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/Tools/Scripts/webkitpy/tool/bot/feeders_unittest.py b/Tools/Scripts/webkitpy/tool/bot/feeders_unittest.py
new file mode 100644
index 0000000..dff48ad
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/feeders_unittest.py
@@ -0,0 +1,80 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from datetime import datetime
+import unittest
+
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.bot.feeders import *
+from webkitpy.tool.mocktool import MockTool
+
+
+class FeedersTest(unittest.TestCase):
+    def test_commit_queue_feeder(self):
+        feeder = CommitQueueFeeder(MockTool())
+        expected_stderr = u"""Warning, attachment 10001 on bug 50000 has invalid committer (non-committer@example.com)
+Warning, attachment 10001 on bug 50000 has invalid committer (non-committer@example.com)
+MOCK setting flag 'commit-queue' to '-' on attachment '10001' with comment 'Rejecting attachment 10001 from commit-queue.' and additional comment 'non-committer@example.com does not have committer permissions according to http://trac.webkit.org/browser/trunk/Tools/Scripts/webkitpy/common/config/committers.py.
+
+- If you do not have committer rights please read http://webkit.org/coding/contributing.html for instructions on how to use bugzilla flags.
+
+- If you have committer rights please correct the error in Tools/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 [10005, 10000]
+Feeding commit-queue items [10005, 10000]
+"""
+        OutputCapture().assert_outputs(self, feeder.feed, expected_stderr=expected_stderr)
+
+    def _mock_attachment(self, is_rollout, attach_date):
+        attachment = Mock()
+        attachment.is_rollout = lambda: is_rollout
+        attachment.attach_date = lambda: attach_date
+        return attachment
+
+    def test_patch_cmp(self):
+        long_ago_date = datetime(1900, 1, 21)
+        recent_date = datetime(2010, 1, 21)
+        attachment1 = self._mock_attachment(is_rollout=False, attach_date=recent_date)
+        attachment2 = self._mock_attachment(is_rollout=False, attach_date=long_ago_date)
+        attachment3 = self._mock_attachment(is_rollout=True, attach_date=recent_date)
+        attachment4 = self._mock_attachment(is_rollout=True, attach_date=long_ago_date)
+        attachments = [attachment1, attachment2, attachment3, attachment4]
+        expected_sort = [attachment4, attachment3, attachment2, attachment1]
+        queue = CommitQueueFeeder(MockTool())
+        attachments.sort(queue._patch_cmp)
+        self.assertEqual(attachments, expected_sort)
+
+    def test_patches_with_acceptable_review_flag(self):
+        class MockPatch(object):
+            def __init__(self, patch_id, review):
+                self.id = patch_id
+                self.review = lambda: review
+
+        feeder = CommitQueueFeeder(MockTool())
+        patches = [MockPatch(1, None), MockPatch(2, '-'), MockPatch(3, "+")]
+        self.assertEquals([patch.id for patch in feeder._patches_with_acceptable_review_flag(patches)], [1, 3])
diff --git a/Tools/Scripts/webkitpy/tool/bot/flakytestreporter.py b/Tools/Scripts/webkitpy/tool/bot/flakytestreporter.py
new file mode 100644
index 0000000..7be4a4a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/flakytestreporter.py
@@ -0,0 +1,199 @@
+# 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 codecs
+import logging
+import os.path
+
+from webkitpy.common.net.layouttestresults import path_for_layout_test, LayoutTestResults
+from webkitpy.common.config import urls
+from webkitpy.tool.bot.botinfo import BotInfo
+from webkitpy.tool.grammar import plural, pluralize, join_with_separators
+
+_log = logging.getLogger(__name__)
+
+
+class FlakyTestReporter(object):
+    def __init__(self, tool, bot_name):
+        self._tool = tool
+        self._bot_name = bot_name
+        self._bot_info = BotInfo(tool)
+
+    def _author_emails_for_test(self, flaky_test):
+        test_path = path_for_layout_test(flaky_test)
+        commit_infos = self._tool.checkout().recent_commit_infos_for_files([test_path])
+        # This ignores authors which are not committers because we don't have their bugzilla_email.
+        return set([commit_info.author().bugzilla_email() for commit_info in commit_infos if commit_info.author()])
+
+    def _bugzilla_email(self):
+        # FIXME: This is kinda a funny way to get the bugzilla email,
+        # we could also just create a Credentials object directly
+        # but some of the Credentials logic is in bugzilla.py too...
+        self._tool.bugs.authenticate()
+        return self._tool.bugs.username
+
+    # FIXME: This should move into common.config
+    _bot_emails = set([
+        "commit-queue@webkit.org",  # commit-queue
+        "eseidel@chromium.org",  # old commit-queue
+        "webkit.review.bot@gmail.com",  # style-queue, sheriff-bot, CrLx/Gtk EWS
+        "buildbot@hotmail.com",  # Win EWS
+        # Mac EWS currently uses eric@webkit.org, but that's not normally a bot
+    ])
+
+    def _lookup_bug_for_flaky_test(self, flaky_test):
+        bugs = self._tool.bugs.queries.fetch_bugs_matching_search(search_string=flaky_test)
+        if not bugs:
+            return None
+        # Match any bugs which are from known bots or the email this bot is using.
+        allowed_emails = self._bot_emails | set([self._bugzilla_email])
+        bugs = filter(lambda bug: bug.reporter_email() in allowed_emails, bugs)
+        if not bugs:
+            return None
+        if len(bugs) > 1:
+            # FIXME: There are probably heuristics we could use for finding
+            # the right bug instead of the first, like open vs. closed.
+            _log.warn("Found %s %s matching '%s' filed by a bot, using the first." % (pluralize('bug', len(bugs)), [bug.id() for bug in bugs], flaky_test))
+        return bugs[0]
+
+    def _view_source_url_for_test(self, test_path):
+        return urls.view_source_url("LayoutTests/%s" % test_path)
+
+    def _create_bug_for_flaky_test(self, flaky_test, author_emails, latest_flake_message):
+        format_values = {
+            'test': flaky_test,
+            'authors': join_with_separators(sorted(author_emails)),
+            'flake_message': latest_flake_message,
+            'test_url': self._view_source_url_for_test(flaky_test),
+            'bot_name': self._bot_name,
+        }
+        title = "Flaky Test: %(test)s" % format_values
+        description = """This is an automatically generated bug from the %(bot_name)s.
+%(test)s has been flaky on the %(bot_name)s.
+
+%(test)s was authored by %(authors)s.
+%(test_url)s
+
+%(flake_message)s
+
+The bots will update this with information from each new failure.
+
+If you believe this bug to be fixed or invalid, feel free to close.  The bots will re-open if the flake re-occurs.
+
+If you would like to track this test fix with another bug, please close this bug as a duplicate.  The bots will follow the duplicate chain when making future comments.
+""" % format_values
+
+        master_flake_bug = 50856  # MASTER: Flaky tests found by the commit-queue
+        return self._tool.bugs.create_bug(title, description,
+            component="Tools / Tests",
+            cc=",".join(author_emails),
+            blocked="50856")
+
+    # This is over-engineered, but it makes for pretty bug messages.
+    def _optional_author_string(self, author_emails):
+        if not author_emails:
+            return ""
+        heading_string = plural('author') if len(author_emails) > 1 else 'author'
+        authors_string = join_with_separators(sorted(author_emails))
+        return " (%s: %s)" % (heading_string, authors_string)
+
+    def _latest_flake_message(self, flaky_result, patch):
+        failure_messages = [failure.message() for failure in flaky_result.failures]
+        flake_message = "The %s just saw %s flake (%s) while processing attachment %s on bug %s." % (self._bot_name, flaky_result.test_name, ", ".join(failure_messages), patch.id(), patch.bug_id())
+        return "%s\n%s" % (flake_message, self._bot_info.summary_text())
+
+    def _results_diff_path_for_test(self, test_path):
+        # FIXME: This is a big hack.  We should get this path from results.json
+        # except that old-run-webkit-tests doesn't produce a results.json
+        # so we just guess at the file path.
+        (test_path_root, _) = os.path.splitext(test_path)
+        return "%s-diffs.txt" % test_path_root
+
+    def _follow_duplicate_chain(self, bug):
+        while bug.is_closed() and bug.duplicate_of():
+            bug = self._tool.bugs.fetch_bug(bug.duplicate_of())
+        return bug
+
+    # Maybe this logic should move into Bugzilla? a reopen=True arg to post_comment?
+    def _update_bug_for_flaky_test(self, bug, latest_flake_message):
+        if bug.is_closed():
+            self._tool.bugs.reopen_bug(bug.id(), latest_flake_message)
+        else:
+            self._tool.bugs.post_comment_to_bug(bug.id(), latest_flake_message)
+
+    # This method is needed because our archive paths include a leading tmp/layout-test-results
+    def _find_in_archive(self, path, archive):
+        for archived_path in archive.namelist():
+            # Archives are currently created with full paths.
+            if archived_path.endswith(path):
+                return archived_path
+        return None
+
+    def _attach_failure_diff(self, flake_bug_id, flaky_test, results_archive_zip):
+        results_diff_path = self._results_diff_path_for_test(flaky_test)
+        # Check to make sure that the path makes sense.
+        # Since we're not actually getting this path from the results.html
+        # there is a chance it's wrong.
+        bot_id = self._tool.status_server.bot_id or "bot"
+        archive_path = self._find_in_archive(results_diff_path, results_archive_zip)
+        if archive_path:
+            results_diff = results_archive_zip.read(archive_path)
+            description = "Failure diff from %s" % bot_id
+            self._tool.bugs.add_attachment_to_bug(flake_bug_id, results_diff, description, filename="failure.diff")
+        else:
+            _log.warn("%s does not exist in results archive, uploading entire archive." % results_diff_path)
+            description = "Archive of layout-test-results from %s" % bot_id
+            # results_archive is a ZipFile object, grab the File object (.fp) to pass to Mechanize for uploading.
+            results_archive_file = results_archive_zip.fp
+            # Rewind the file object to start (since Mechanize won't do that automatically)
+            # See https://bugs.webkit.org/show_bug.cgi?id=54593
+            results_archive_file.seek(0)
+            self._tool.bugs.add_attachment_to_bug(flake_bug_id, results_archive_file, description, filename="layout-test-results.zip")
+
+    def report_flaky_tests(self, patch, flaky_test_results, results_archive):
+        message = "The %s encountered the following flaky tests while processing attachment %s:\n\n" % (self._bot_name, patch.id())
+        for flaky_result in flaky_test_results:
+            flaky_test = flaky_result.test_name
+            bug = self._lookup_bug_for_flaky_test(flaky_test)
+            latest_flake_message = self._latest_flake_message(flaky_result, patch)
+            author_emails = self._author_emails_for_test(flaky_test)
+            if not bug:
+                _log.info("Bug does not already exist for %s, creating." % flaky_test)
+                flake_bug_id = self._create_bug_for_flaky_test(flaky_test, author_emails, latest_flake_message)
+            else:
+                bug = self._follow_duplicate_chain(bug)
+                # FIXME: Ideally we'd only make one comment per flake, not two.  But that's not possible
+                # in all cases (e.g. when reopening), so for now file attachment and comment are separate.
+                self._update_bug_for_flaky_test(bug, latest_flake_message)
+                flake_bug_id = bug.id()
+
+            self._attach_failure_diff(flake_bug_id, flaky_test, results_archive)
+            message += "%s bug %s%s\n" % (flaky_test, flake_bug_id, self._optional_author_string(author_emails))
+
+        message += "The %s is continuing to process your patch." % self._bot_name
+        self._tool.bugs.post_comment_to_bug(patch.bug_id(), message)
diff --git a/Tools/Scripts/webkitpy/tool/bot/flakytestreporter_unittest.py b/Tools/Scripts/webkitpy/tool/bot/flakytestreporter_unittest.py
new file mode 100644
index 0000000..eeb06c3
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/flakytestreporter_unittest.py
@@ -0,0 +1,168 @@
+# 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.config.committers import Committer
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.layout_tests.models import test_results
+from webkitpy.layout_tests.models import test_failures
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.bot.flakytestreporter import FlakyTestReporter
+from webkitpy.tool.mocktool import MockTool
+from webkitpy.common.net.statusserver_mock import MockStatusServer
+
+
+# Creating fake CommitInfos is a pain, so we use a mock one here.
+class MockCommitInfo(object):
+    def __init__(self, author_email):
+        self._author_email = author_email
+
+    def author(self):
+        # It's definitely possible to have commits with authors who
+        # are not in our committers.py list.
+        if not self._author_email:
+            return None
+        return Committer("Mock Committer", self._author_email)
+
+
+class FlakyTestReporterTest(unittest.TestCase):
+    def _mock_test_result(self, testname):
+        return test_results.TestResult(testname, [test_failures.FailureTextMismatch()])
+
+    def _assert_emails_for_test(self, emails):
+        tool = MockTool()
+        reporter = FlakyTestReporter(tool, 'dummy-queue')
+        commit_infos = [MockCommitInfo(email) for email in emails]
+        tool.checkout().recent_commit_infos_for_files = lambda paths: set(commit_infos)
+        self.assertEqual(reporter._author_emails_for_test([]), set(emails))
+
+    def test_author_emails_for_test(self):
+        self._assert_emails_for_test([])
+        self._assert_emails_for_test(["test1@test.com", "test1@test.com"])
+        self._assert_emails_for_test(["test1@test.com", "test2@test.com"])
+
+    def test_create_bug_for_flaky_test(self):
+        reporter = FlakyTestReporter(MockTool(), 'dummy-queue')
+        expected_stderr = """MOCK create_bug
+bug_title: Flaky Test: foo/bar.html
+bug_description: This is an automatically generated bug from the dummy-queue.
+foo/bar.html has been flaky on the dummy-queue.
+
+foo/bar.html was authored by test@test.com.
+http://trac.webkit.org/browser/trunk/LayoutTests/foo/bar.html
+
+FLAKE_MESSAGE
+
+The bots will update this with information from each new failure.
+
+If you believe this bug to be fixed or invalid, feel free to close.  The bots will re-open if the flake re-occurs.
+
+If you would like to track this test fix with another bug, please close this bug as a duplicate.  The bots will follow the duplicate chain when making future comments.
+
+component: Tools / Tests
+cc: test@test.com
+blocked: 50856
+"""
+        OutputCapture().assert_outputs(self, reporter._create_bug_for_flaky_test, ['foo/bar.html', ['test@test.com'], 'FLAKE_MESSAGE'], expected_stderr=expected_stderr)
+
+    def test_follow_duplicate_chain(self):
+        tool = MockTool()
+        reporter = FlakyTestReporter(tool, 'dummy-queue')
+        bug = tool.bugs.fetch_bug(50004)
+        self.assertEqual(reporter._follow_duplicate_chain(bug).id(), 50002)
+
+    def test_report_flaky_tests_creating_bug(self):
+        tool = MockTool()
+        tool.filesystem = MockFileSystem({"/mock-results/foo/bar-diffs.txt": "mock"})
+        tool.status_server = MockStatusServer(bot_id="mock-bot-id")
+        reporter = FlakyTestReporter(tool, 'dummy-queue')
+        reporter._lookup_bug_for_flaky_test = lambda bug_id: None
+        patch = tool.bugs.fetch_attachment(10000)
+        expected_stderr = """MOCK create_bug
+bug_title: Flaky Test: foo/bar.html
+bug_description: This is an automatically generated bug from the dummy-queue.
+foo/bar.html has been flaky on the dummy-queue.
+
+foo/bar.html was authored by abarth@webkit.org.
+http://trac.webkit.org/browser/trunk/LayoutTests/foo/bar.html
+
+The dummy-queue just saw foo/bar.html flake (text diff) while processing attachment 10000 on bug 50000.
+Bot: mock-bot-id  Port: MockPort  Platform: MockPlatform 1.0
+
+The bots will update this with information from each new failure.
+
+If you believe this bug to be fixed or invalid, feel free to close.  The bots will re-open if the flake re-occurs.
+
+If you would like to track this test fix with another bug, please close this bug as a duplicate.  The bots will follow the duplicate chain when making future comments.
+
+component: Tools / Tests
+cc: abarth@webkit.org
+blocked: 50856
+MOCK add_attachment_to_bug: bug_id=60001, description=Failure diff from mock-bot-id filename=failure.diff mimetype=None
+MOCK bug comment: bug_id=50000, cc=None
+--- Begin comment ---
+The dummy-queue encountered the following flaky tests while processing attachment 10000:
+
+foo/bar.html bug 60001 (author: abarth@webkit.org)
+The dummy-queue is continuing to process your patch.
+--- End comment ---
+
+"""
+        test_results = [self._mock_test_result('foo/bar.html')]
+
+        class MockZipFile(object):
+            def read(self, path):
+                return ""
+
+            def namelist(self):
+                return ['foo/bar-diffs.txt']
+
+        OutputCapture().assert_outputs(self, reporter.report_flaky_tests, [patch, test_results, MockZipFile()], expected_stderr=expected_stderr)
+
+    def test_optional_author_string(self):
+        reporter = FlakyTestReporter(MockTool(), 'dummy-queue')
+        self.assertEqual(reporter._optional_author_string([]), "")
+        self.assertEqual(reporter._optional_author_string(["foo@bar.com"]), " (author: foo@bar.com)")
+        self.assertEqual(reporter._optional_author_string(["a@b.com", "b@b.com"]), " (authors: a@b.com and b@b.com)")
+
+    def test_results_diff_path_for_test(self):
+        reporter = FlakyTestReporter(MockTool(), 'dummy-queue')
+        self.assertEqual(reporter._results_diff_path_for_test("test.html"), "test-diffs.txt")
+
+    def test_find_in_archive(self):
+        reporter = FlakyTestReporter(MockTool(), 'dummy-queue')
+
+        class MockZipFile(object):
+            def namelist(self):
+                return ["tmp/layout-test-results/foo/bar-diffs.txt"]
+
+        reporter._find_in_archive("foo/bar-diffs.txt", MockZipFile())
+        # This is not ideal, but its
+        reporter._find_in_archive("txt", MockZipFile())
diff --git a/Tools/Scripts/webkitpy/tool/bot/irc_command.py b/Tools/Scripts/webkitpy/tool/bot/irc_command.py
new file mode 100644
index 0000000..1c061a8
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/irc_command.py
@@ -0,0 +1,250 @@
+# 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 itertools
+import random
+import re
+
+from webkitpy.common.config import irc as config_irc
+from webkitpy.common.config import urls
+from webkitpy.common.config.committers import CommitterList
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.tool.bot.queueengine import TerminateQueue
+from webkitpy.tool.grammar import join_with_separators
+
+
+def _post_error_and_check_for_bug_url(tool, nicks_string, exception):
+    tool.irc().post("%s" % exception)
+    bug_id = urls.parse_bug_id(exception.output)
+    if bug_id:
+        bug_url = tool.bugs.bug_url_for_bug_id(bug_id)
+        tool.irc().post("%s: Ugg...  Might have created %s" % (nicks_string, bug_url))
+
+
+# FIXME: Merge with Command?
+class IRCCommand(object):
+    def execute(self, nick, args, tool, sheriff):
+        raise NotImplementedError, "subclasses must implement"
+
+
+class Restart(IRCCommand):
+    def execute(self, nick, args, tool, sheriff):
+        tool.irc().post("Restarting...")
+        raise TerminateQueue()
+
+
+class Rollout(IRCCommand):
+    def _extract_revisions(self, arg):
+
+        revision_list = []
+        possible_revisions = arg.split(",")
+        for revision in possible_revisions:
+            revision = revision.strip()
+            if not revision:
+                continue
+            revision = revision.lstrip("r")
+            # If one part of the arg isn't in the correct format,
+            # then none of the arg should be considered a revision.
+            if not revision.isdigit():
+                return None
+            revision_list.append(int(revision))
+        return revision_list
+
+    def _parse_args(self, args):
+        if not args:
+            return (None, None)
+
+        svn_revision_list = []
+        remaining_args = args[:]
+        # First process all revisions.
+        while remaining_args:
+            new_revisions = self._extract_revisions(remaining_args[0])
+            if not new_revisions:
+                break
+            svn_revision_list += new_revisions
+            remaining_args = remaining_args[1:]
+
+        # Was there a revision number?
+        if not len(svn_revision_list):
+            return (None, None)
+
+        # Everything left is the reason.
+        rollout_reason = " ".join(remaining_args)
+        return svn_revision_list, rollout_reason
+
+    def _responsible_nicknames_from_revisions(self, tool, sheriff, svn_revision_list):
+        commit_infos = map(tool.checkout().commit_info_for_revision, svn_revision_list)
+        nickname_lists = map(sheriff.responsible_nicknames_from_commit_info, commit_infos)
+        return sorted(set(itertools.chain(*nickname_lists)))
+
+    def _nicks_string(self, tool, sheriff, requester_nick, svn_revision_list):
+        # FIXME: _parse_args guarentees that our svn_revision_list is all numbers.
+        # However, it's possible our checkout will not include one of the revisions,
+        # so we may need to catch exceptions from commit_info_for_revision here.
+        target_nicks = [requester_nick] + self._responsible_nicknames_from_revisions(tool, sheriff, svn_revision_list)
+        return ", ".join(target_nicks)
+
+    def _update_working_copy(self, tool):
+        tool.scm().ensure_clean_working_directory(force_clean=True)
+        tool.executive.run_and_throw_if_fail(tool.port().update_webkit_command(), quiet=True, cwd=tool.scm().checkout_root)
+
+    def execute(self, nick, args, tool, sheriff):
+        svn_revision_list, rollout_reason = self._parse_args(args)
+
+        if (not svn_revision_list or not rollout_reason):
+            # return is equivalent to an irc().post(), but makes for easier unit testing.
+            return "%s: Usage: rollout SVN_REVISION [SVN_REVISIONS] REASON" % nick
+
+        revision_urls_string = join_with_separators([urls.view_revision_url(revision) for revision in svn_revision_list])
+        tool.irc().post("%s: Preparing rollout for %s ..." % (nick, revision_urls_string))
+
+        self._update_working_copy(tool)
+
+        # FIXME: IRCCommand should bind to a tool and have a self._tool like Command objects do.
+        # Likewise we should probably have a self._sheriff.
+        nicks_string = self._nicks_string(tool, sheriff, nick, svn_revision_list)
+
+        try:
+            complete_reason = "%s (Requested by %s on %s)." % (
+                rollout_reason, nick, config_irc.channel)
+            bug_id = sheriff.post_rollout_patch(svn_revision_list, complete_reason)
+            bug_url = tool.bugs.bug_url_for_bug_id(bug_id)
+            tool.irc().post("%s: Created rollout: %s" % (nicks_string, bug_url))
+        except ScriptError, e:
+            tool.irc().post("%s: Failed to create rollout patch:" % nicks_string)
+            _post_error_and_check_for_bug_url(tool, nicks_string, e)
+
+
+class RollChromiumDEPS(IRCCommand):
+    def _parse_args(self, args):
+        if not args:
+            return
+        revision = args[0].lstrip("r")
+        if not revision.isdigit():
+            return
+        return revision
+
+    def execute(self, nick, args, tool, sheriff):
+        revision = self._parse_args(args)
+
+        roll_target = "r%s" % revision if revision else "last-known good revision"
+        tool.irc().post("%s: Rolling Chromium DEPS to %s" % (nick, roll_target))
+
+        try:
+            bug_id = sheriff.post_chromium_deps_roll(revision, roll_target)
+            bug_url = tool.bugs.bug_url_for_bug_id(bug_id)
+            tool.irc().post("%s: Created DEPS roll: %s" % (nick, bug_url))
+        except ScriptError, e:
+            match = re.search(r"Current Chromium DEPS revision \d+ is newer than \d+\.", e.output)
+            if match:
+                tool.irc().post("%s: %s" % (nick, match.group(0)))
+                return
+            tool.irc().post("%s: Failed to create DEPS roll:" % nick)
+            _post_error_and_check_for_bug_url(tool, nick, e)
+
+
+class Help(IRCCommand):
+    def execute(self, nick, args, tool, sheriff):
+        return "%s: Available commands: %s" % (nick, ", ".join(sorted(visible_commands.keys())))
+
+
+class Hi(IRCCommand):
+    def execute(self, nick, args, tool, sheriff):
+        quips = tool.bugs.quips()
+        quips.append('"Only you can prevent forest fires." -- Smokey the Bear')
+        return random.choice(quips)
+
+
+class Whois(IRCCommand):
+    def _nick_or_full_record(self, contributor):
+        if contributor.irc_nicknames:
+            return ', '.join(contributor.irc_nicknames)
+        return unicode(contributor)
+
+    def execute(self, nick, args, tool, sheriff):
+        if len(args) != 1:
+            return "%s: Usage: whois SEARCH_STRING" % nick
+        search_string = args[0]
+        # FIXME: We should get the ContributorList off the tool somewhere.
+        contributors = CommitterList().contributors_by_search_string(search_string)
+        if not contributors:
+            return "%s: Sorry, I don't know any contributors matching '%s'." % (nick, search_string)
+        if len(contributors) > 5:
+            return "%s: More than 5 contributors match '%s', could you be more specific?" % (nick, search_string)
+        if len(contributors) == 1:
+            contributor = contributors[0]
+            if not contributor.irc_nicknames:
+                return "%s: %s hasn't told me their nick. Boo hoo :-(" % (nick, contributor)
+            if contributor.emails and search_string.lower() not in map(lambda email: email.lower(), contributor.emails):
+                formattedEmails = ', '.join(contributor.emails)
+                return "%s: %s is %s (%s). Why do you ask?" % (nick, search_string, self._nick_or_full_record(contributor), formattedEmails)
+            else:
+                return "%s: %s is %s. Why do you ask?" % (nick, search_string, self._nick_or_full_record(contributor))
+        contributor_nicks = map(self._nick_or_full_record, contributors)
+        contributors_string = join_with_separators(contributor_nicks, only_two_separator=" or ", last_separator=', or ')
+        return "%s: I'm not sure who you mean?  %s could be '%s'." % (nick, contributors_string, search_string)
+
+
+class CreateBug(IRCCommand):
+    def execute(self, nick, args, tool, sheriff):
+        if not args:
+            return "%s: Usage: create-bug BUG_TITLE" % nick
+
+        bug_title = " ".join(args)
+        bug_description = "%s\nRequested by %s on %s." % (bug_title, nick, config_irc.channel)
+
+        # There happens to be a committers list hung off of Bugzilla, so
+        # re-using that one makes things easiest for now.
+        requester = tool.bugs.committers.contributor_by_irc_nickname(nick)
+        requester_email = requester.bugzilla_email() if requester else None
+
+        try:
+            bug_id = tool.bugs.create_bug(bug_title, bug_description, cc=requester_email, assignee=requester_email)
+            bug_url = tool.bugs.bug_url_for_bug_id(bug_id)
+            return "%s: Created bug: %s" % (nick, bug_url)
+        except Exception, e:
+            return "%s: Failed to create bug:\n%s" % (nick, e)
+
+
+# FIXME: Lame.  We should have an auto-registering CommandCenter.
+visible_commands = {
+    "help": Help,
+    "hi": Hi,
+    "restart": Restart,
+    "rollout": Rollout,
+    "whois": Whois,
+    "create-bug": CreateBug,
+    "roll-chromium-deps": RollChromiumDEPS,
+}
+
+# Add revert as an "easter egg" command. Why?
+# revert is the same as rollout and it would be confusing to list both when
+# they do the same thing. However, this command is a very natural thing for
+# people to use and it seems silly to have them hunt around for "rollout" instead.
+commands = visible_commands.copy()
+commands["revert"] = Rollout
diff --git a/Tools/Scripts/webkitpy/tool/bot/irc_command_unittest.py b/Tools/Scripts/webkitpy/tool/bot/irc_command_unittest.py
new file mode 100644
index 0000000..4dec669
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/irc_command_unittest.py
@@ -0,0 +1,117 @@
+# 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.system.outputcapture import OutputCapture
+from webkitpy.tool.bot.irc_command import *
+from webkitpy.tool.mocktool import MockTool
+from webkitpy.common.system.executive_mock import MockExecutive
+
+
+class IRCCommandTest(unittest.TestCase):
+    def test_whois(self):
+        whois = Whois()
+        self.assertEquals("tom: Usage: whois SEARCH_STRING",
+                          whois.execute("tom", [], None, None))
+        self.assertEquals("tom: Usage: whois SEARCH_STRING",
+                          whois.execute("tom", ["Adam", "Barth"], None, None))
+        self.assertEquals("tom: Sorry, I don't know any contributors matching 'unknown@example.com'.",
+                          whois.execute("tom", ["unknown@example.com"], None, None))
+        self.assertEquals("tom: tonyg@chromium.org is tonyg-cr. Why do you ask?",
+                          whois.execute("tom", ["tonyg@chromium.org"], None, None))
+        self.assertEquals("tom: TonyG@Chromium.org is tonyg-cr. Why do you ask?",
+                          whois.execute("tom", ["TonyG@Chromium.org"], None, None))
+        self.assertEquals("tom: rniwa is rniwa (rniwa@webkit.org). Why do you ask?",
+                          whois.execute("tom", ["rniwa"], None, None))
+        self.assertEquals("tom: lopez is xan (xan.lopez@gmail.com, xan@gnome.org, xan@webkit.org, xlopez@igalia.com). Why do you ask?",
+                          whois.execute("tom", ["lopez"], None, None))
+        self.assertEquals('tom: "Vicki Murley" <vicki@apple.com> hasn\'t told me their nick. Boo hoo :-(',
+                          whois.execute("tom", ["vicki@apple.com"], None, None))
+        self.assertEquals('tom: I\'m not sure who you mean?  gavinp or gbarra could be \'Gavin\'.',
+                          whois.execute("tom", ["Gavin"], None, None))
+        self.assertEquals('tom: More than 5 contributors match \'david\', could you be more specific?',
+                          whois.execute("tom", ["david"], None, None))
+
+    def test_create_bug(self):
+        create_bug = CreateBug()
+        self.assertEquals("tom: Usage: create-bug BUG_TITLE",
+                          create_bug.execute("tom", [], None, None))
+
+        example_args = ["sherrif-bot", "should", "have", "a", "create-bug", "command"]
+        tool = MockTool()
+
+        # MockBugzilla has a create_bug, but it logs to stderr, this avoids any logging.
+        tool.bugs.create_bug = lambda a, b, cc=None, assignee=None: 50004
+        self.assertEquals("tom: Created bug: http://example.com/50004",
+                          create_bug.execute("tom", example_args, tool, None))
+
+        def mock_create_bug(title, description, cc=None, assignee=None):
+            raise Exception("Exception from bugzilla!")
+        tool.bugs.create_bug = mock_create_bug
+        self.assertEquals("tom: Failed to create bug:\nException from bugzilla!",
+                          create_bug.execute("tom", example_args, tool, None))
+
+    def test_roll_chromium_deps(self):
+        roll = RollChromiumDEPS()
+        self.assertEquals(None, roll._parse_args([]))
+        self.assertEquals("1234", roll._parse_args(["1234"]))
+
+    def test_rollout_updates_working_copy(self):
+        rollout = Rollout()
+        tool = MockTool()
+        tool.executive = MockExecutive(should_log=True)
+        expected_stderr = "MOCK run_and_throw_if_fail: ['mock-update-webkit'], cwd=/mock-checkout\n"
+        OutputCapture().assert_outputs(self, rollout._update_working_copy, [tool], expected_stderr=expected_stderr)
+
+    def test_rollout(self):
+        rollout = Rollout()
+        self.assertEquals(([1234], "testing foo"),
+                          rollout._parse_args(["1234", "testing", "foo"]))
+
+        self.assertEquals(([554], "testing foo"),
+                          rollout._parse_args(["r554", "testing", "foo"]))
+
+        self.assertEquals(([556, 792], "testing foo"),
+                          rollout._parse_args(["r556", "792", "testing", "foo"]))
+
+        self.assertEquals(([128, 256], "testing foo"),
+                          rollout._parse_args(["r128,r256", "testing", "foo"]))
+
+        self.assertEquals(([512, 1024, 2048], "testing foo"),
+                          rollout._parse_args(["512,", "1024,2048", "testing", "foo"]))
+
+        # Test invalid argument parsing:
+        self.assertEquals((None, None), rollout._parse_args([]))
+        self.assertEquals((None, None), rollout._parse_args(["--bar", "1234"]))
+
+        # Invalid arguments result in the USAGE message.
+        self.assertEquals("tom: Usage: rollout SVN_REVISION [SVN_REVISIONS] REASON",
+                          rollout.execute("tom", [], None, None))
+
+        # FIXME: We need a better way to test IRCCommands which call tool.irc().post()
diff --git a/Tools/Scripts/webkitpy/tool/bot/ircbot.py b/Tools/Scripts/webkitpy/tool/bot/ircbot.py
new file mode 100644
index 0000000..0c45b97
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/ircbot.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.
+
+from webkitpy.tool.bot.queueengine import TerminateQueue
+from webkitpy.tool.bot.irc_command import IRCCommand
+from webkitpy.common.net.irc.ircbot import IRCBotDelegate
+from webkitpy.common.thread.threadedmessagequeue import ThreadedMessageQueue
+
+
+class _IRCThreadTearoff(IRCBotDelegate):
+    def __init__(self, name, password, message_queue, wakeup_event):
+        self._name = name
+        self._password = password
+        self._message_queue = message_queue
+        self._wakeup_event = wakeup_event
+
+    # IRCBotDelegate methods
+
+    def irc_message_received(self, nick, message):
+        self._message_queue.post([nick, message])
+        self._wakeup_event.set()
+
+    def irc_nickname(self):
+        return self._name
+
+    def irc_password(self):
+        return self._password
+
+
+class Eliza(IRCCommand):
+    therapist = None
+
+    def __init__(self):
+        if not self.therapist:
+            import webkitpy.thirdparty.autoinstalled.eliza as eliza
+            Eliza.therapist = eliza.eliza()
+
+    def execute(self, nick, args, tool, sheriff):
+        return "%s: %s" % (nick, self.therapist.respond(" ".join(args)))
+
+
+class IRCBot(object):
+    def __init__(self, name, tool, agent, commands):
+        self._name = name
+        self._tool = tool
+        self._agent = agent
+        self._message_queue = ThreadedMessageQueue()
+        self._commands = commands
+
+    def irc_delegate(self):
+        return _IRCThreadTearoff(self._name, self._tool.irc_password,
+            self._message_queue, self._tool.wakeup_event)
+
+    def _parse_command_and_args(self, request):
+        tokenized_request = request.strip().split(" ")
+        command = self._commands.get(tokenized_request[0])
+        args = tokenized_request[1:]
+        if not command:
+            # Give the peoples someone to talk with.
+            command = Eliza
+            args = tokenized_request
+        return (command, args)
+
+    def process_message(self, requester_nick, request):
+        command, args = self._parse_command_and_args(request)
+        try:
+            response = command().execute(requester_nick, args, self._tool, self._agent)
+            if response:
+                self._tool.irc().post(response)
+        except TerminateQueue:
+            raise
+        # This will catch everything else. SystemExit and KeyboardInterrupt are not subclasses of Exception, so we won't catch those.
+        except Exception, e:
+            self._tool.irc().post("Exception executing command: %s" % e)
+
+    def process_pending_messages(self):
+        (messages, is_running) = self._message_queue.take_all()
+        for message in messages:
+            (nick, request) = message
+            self.process_message(nick, request)
diff --git a/Tools/Scripts/webkitpy/tool/bot/ircbot_unittest.py b/Tools/Scripts/webkitpy/tool/bot/ircbot_unittest.py
new file mode 100644
index 0000000..ce9a76b
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/ircbot_unittest.py
@@ -0,0 +1,163 @@
+# 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 random
+
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.tool.bot import irc_command
+from webkitpy.tool.bot.queueengine import TerminateQueue
+from webkitpy.tool.bot.sheriff import Sheriff
+from webkitpy.tool.bot.ircbot import IRCBot
+from webkitpy.tool.bot.ircbot import Eliza
+from webkitpy.tool.bot.sheriff_unittest import MockSheriffBot
+from webkitpy.tool.mocktool import MockTool
+
+
+def run(message):
+    tool = MockTool()
+    tool.ensure_irc_connected(None)
+    bot = IRCBot("sheriffbot", tool, Sheriff(tool, MockSheriffBot()), irc_command.commands)
+    bot._message_queue.post(["mock_nick", message])
+    bot.process_pending_messages()
+
+
+class IRCBotTest(unittest.TestCase):
+    def test_eliza(self):
+        eliza = Eliza()
+        eliza.execute("tom", "hi", None, None)
+        eliza.execute("tom", "bye", None, None)
+
+    def test_parse_command_and_args(self):
+        tool = MockTool()
+        bot = IRCBot("sheriffbot", tool, Sheriff(tool, MockSheriffBot()), irc_command.commands)
+        self.assertEqual(bot._parse_command_and_args(""), (Eliza, [""]))
+        self.assertEqual(bot._parse_command_and_args("   "), (Eliza, [""]))
+        self.assertEqual(bot._parse_command_and_args(" hi "), (irc_command.Hi, []))
+        self.assertEqual(bot._parse_command_and_args(" hi there "), (irc_command.Hi, ["there"]))
+
+    def test_exception_during_command(self):
+        tool = MockTool()
+        tool.ensure_irc_connected(None)
+        bot = IRCBot("sheriffbot", tool, Sheriff(tool, MockSheriffBot()), irc_command.commands)
+
+        class CommandWithException(object):
+            def execute(self, nick, args, tool, sheriff):
+                raise Exception("mock_exception")
+
+        bot._parse_command_and_args = lambda request: (CommandWithException, [])
+        expected_stderr = 'MOCK: irc.post: Exception executing command: mock_exception\n'
+        OutputCapture().assert_outputs(self, bot.process_message, args=["mock_nick", "ignored message"], expected_stderr=expected_stderr)
+
+        class CommandWithException(object):
+            def execute(self, nick, args, tool, sheriff):
+                raise KeyboardInterrupt()
+
+        bot._parse_command_and_args = lambda request: (CommandWithException, [])
+        # KeyboardInterrupt and SystemExit are not subclasses of Exception and thus correctly will not be caught.
+        OutputCapture().assert_outputs(self, bot.process_message, args=["mock_nick", "ignored message"], expected_exception=KeyboardInterrupt)
+
+    def test_hi(self):
+        random.seed(23324)
+        expected_stderr = 'MOCK: irc.post: "Only you can prevent forest fires." -- Smokey the Bear\n'
+        OutputCapture().assert_outputs(self, run, args=["hi"], expected_stderr=expected_stderr)
+
+    def test_help(self):
+        expected_stderr = "MOCK: irc.post: mock_nick: Available commands: create-bug, help, hi, restart, roll-chromium-deps, rollout, whois\n"
+        OutputCapture().assert_outputs(self, run, args=["help"], expected_stderr=expected_stderr)
+
+    def test_restart(self):
+        expected_stderr = "MOCK: irc.post: Restarting...\n"
+        OutputCapture().assert_outputs(self, run, args=["restart"], expected_stderr=expected_stderr, expected_exception=TerminateQueue)
+
+    def test_rollout(self):
+        expected_stderr = "MOCK: irc.post: mock_nick: Preparing rollout for http://trac.webkit.org/changeset/21654 ...\nMOCK: irc.post: mock_nick, abarth, darin, eseidel: Created rollout: http://example.com/36936\n"
+        OutputCapture().assert_outputs(self, run, args=["rollout 21654 This patch broke the world"], expected_stderr=expected_stderr)
+
+    def test_revert(self):
+        expected_stderr = "MOCK: irc.post: mock_nick: Preparing rollout for http://trac.webkit.org/changeset/21654 ...\nMOCK: irc.post: mock_nick, abarth, darin, eseidel: Created rollout: http://example.com/36936\n"
+        OutputCapture().assert_outputs(self, run, args=["revert 21654 This patch broke the world"], expected_stderr=expected_stderr)
+
+    def test_roll_chromium_deps(self):
+        expected_stderr = "MOCK: irc.post: mock_nick: Rolling Chromium DEPS to r21654\nMOCK: irc.post: mock_nick: Created DEPS roll: http://example.com/36936\n"
+        OutputCapture().assert_outputs(self, run, args=["roll-chromium-deps 21654"], expected_stderr=expected_stderr)
+
+    def test_roll_chromium_deps_to_lkgr(self):
+        expected_stderr = "MOCK: irc.post: mock_nick: Rolling Chromium DEPS to last-known good revision\nMOCK: irc.post: mock_nick: Created DEPS roll: http://example.com/36936\n"
+        OutputCapture().assert_outputs(self, run, args=["roll-chromium-deps"], expected_stderr=expected_stderr)
+
+    def test_multi_rollout(self):
+        expected_stderr = "MOCK: irc.post: mock_nick: Preparing rollout for http://trac.webkit.org/changeset/21654, http://trac.webkit.org/changeset/21655, and http://trac.webkit.org/changeset/21656 ...\nMOCK: irc.post: mock_nick, abarth, darin, eseidel: Created rollout: http://example.com/36936\n"
+        OutputCapture().assert_outputs(self, run, args=["rollout 21654 21655 21656 This 21654 patch broke the world"], expected_stderr=expected_stderr)
+
+    def test_rollout_with_r_in_svn_revision(self):
+        expected_stderr = "MOCK: irc.post: mock_nick: Preparing rollout for http://trac.webkit.org/changeset/21654 ...\nMOCK: irc.post: mock_nick, abarth, darin, eseidel: Created rollout: http://example.com/36936\n"
+        OutputCapture().assert_outputs(self, run, args=["rollout r21654 This patch broke the world"], expected_stderr=expected_stderr)
+
+    def test_multi_rollout_with_r_in_svn_revision(self):
+        expected_stderr = "MOCK: irc.post: mock_nick: Preparing rollout for http://trac.webkit.org/changeset/21654, http://trac.webkit.org/changeset/21655, and http://trac.webkit.org/changeset/21656 ...\nMOCK: irc.post: mock_nick, abarth, darin, eseidel: Created rollout: http://example.com/36936\n"
+        OutputCapture().assert_outputs(self, run, args=["rollout r21654 21655 r21656 This r21654 patch broke the world"], expected_stderr=expected_stderr)
+
+    def test_rollout_bananas(self):
+        expected_stderr = "MOCK: irc.post: mock_nick: Usage: rollout SVN_REVISION [SVN_REVISIONS] REASON\n"
+        OutputCapture().assert_outputs(self, run, args=["rollout bananas"], expected_stderr=expected_stderr)
+
+    def test_rollout_invalidate_revision(self):
+        # When folks pass junk arguments, we should just spit the usage back at them.
+        expected_stderr = "MOCK: irc.post: mock_nick: Usage: rollout SVN_REVISION [SVN_REVISIONS] REASON\n"
+        OutputCapture().assert_outputs(self, run,
+                                       args=["rollout --component=Tools 21654"],
+                                       expected_stderr=expected_stderr)
+
+    def test_rollout_invalidate_reason(self):
+        # FIXME: I'm slightly confused as to why this doesn't return the USAGE message.
+        expected_stderr = """MOCK: irc.post: mock_nick: Preparing rollout for http://trac.webkit.org/changeset/21654 ...
+MOCK: irc.post: mock_nick, abarth, darin, eseidel: Failed to create rollout patch:
+MOCK: irc.post: The rollout reason may not begin with - (\"-bad (Requested by mock_nick on #webkit).\").
+"""
+        OutputCapture().assert_outputs(self, run,
+                                       args=["rollout 21654 -bad"],
+                                       expected_stderr=expected_stderr)
+
+    def test_multi_rollout_invalidate_reason(self):
+        expected_stderr = """MOCK: irc.post: mock_nick: Preparing rollout for http://trac.webkit.org/changeset/21654, http://trac.webkit.org/changeset/21655, and http://trac.webkit.org/changeset/21656 ...
+MOCK: irc.post: mock_nick, abarth, darin, eseidel: Failed to create rollout patch:
+MOCK: irc.post: The rollout reason may not begin with - (\"-bad (Requested by mock_nick on #webkit).\").
+"""
+        OutputCapture().assert_outputs(self, run,
+                                       args=["rollout "
+                                             "21654 21655 r21656 -bad"],
+                                       expected_stderr=expected_stderr)
+
+    def test_rollout_no_reason(self):
+        expected_stderr = "MOCK: irc.post: mock_nick: Usage: rollout SVN_REVISION [SVN_REVISIONS] REASON\n"
+        OutputCapture().assert_outputs(self, run, args=["rollout 21654"], expected_stderr=expected_stderr)
+
+    def test_multi_rollout_no_reason(self):
+        expected_stderr = "MOCK: irc.post: mock_nick: Usage: rollout SVN_REVISION [SVN_REVISIONS] REASON\n"
+        OutputCapture().assert_outputs(self, run, args=["rollout 21654 21655 r21656"], expected_stderr=expected_stderr)
diff --git a/Tools/Scripts/webkitpy/tool/bot/layouttestresultsreader.py b/Tools/Scripts/webkitpy/tool/bot/layouttestresultsreader.py
new file mode 100644
index 0000000..94a70b2
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/layouttestresultsreader.py
@@ -0,0 +1,101 @@
+# Copyright (c) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.net.layouttestresults import LayoutTestResults
+from webkitpy.common.net.unittestresults import UnitTestResults
+from webkitpy.common.system.deprecated_logging import error, log
+from webkitpy.tool.steps.runtests import RunTests
+
+
+class LayoutTestResultsReader(object):
+    def __init__(self, tool, archive_directory):
+        self._tool = tool
+        self._archive_directory = archive_directory
+
+    # FIXME: This exists for mocking, but should instead be mocked via
+    # tool.filesystem.read_text_file.  They have different error handling at the moment.
+    def _read_file_contents(self, path):
+        try:
+            return self._tool.filesystem.read_text_file(path)
+        except IOError, e:  # File does not exist or can't be read.
+            return None
+
+    # FIXME: This logic should move to the port object.
+    def _create_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 _create_unit_test_results(self):
+        results_path = self._tool.port().unit_tests_results_path()
+        if not results_path:
+            return None
+        results_xml = self._read_file_contents(results_path)
+        if not results_xml:
+            return None
+        return UnitTestResults.results_from_string(results_xml)
+
+    def results(self):
+        layout_test_results = self._create_layout_test_results()
+        unit_test_results = self._create_unit_test_results()
+        if layout_test_results:
+            # FIXME: We should not have to set failure_limit_count, but we
+            # do until run-webkit-tests can be updated save off the value
+            # of --exit-after-N-failures in results.html/results.json.
+            # https://bugs.webkit.org/show_bug.cgi?id=58481
+            layout_test_results.set_failure_limit_count(RunTests.NON_INTERACTIVE_FAILURE_LIMIT_COUNT)
+            if unit_test_results:
+                layout_test_results.add_unit_test_failures(unit_test_results)
+        return layout_test_results
+
+    def _results_directory(self):
+        results_path = self._tool.port().layout_tests_results_path()
+        # FIXME: This is wrong in two ways:
+        # 1. It assumes that results.html is at the top level of the results tree.
+        # 2. This uses the "old" ports.py infrastructure instead of the new layout_tests/port
+        # which will not support Chromium.  However the new arch doesn't work with old-run-webkit-tests
+        # so we have to use this for now.
+        return self._tool.filesystem.dirname(results_path)
+
+    def archive(self, patch):
+        results_directory = self._results_directory()
+        results_name, _ = self._tool.filesystem.splitext(self._tool.filesystem.basename(results_directory))
+        # Note: We name the zip with the bug_id instead of patch_id to match work_item_log_path().
+        zip_path = self._tool.workspace.find_unused_filename(self._archive_directory, "%s-%s" % (patch.bug_id(), results_name), "zip")
+        if not zip_path:
+            return None
+        if not self._tool.filesystem.isdir(results_directory):
+            log("%s does not exist, not archiving." % results_directory)
+            return None
+        archive = self._tool.workspace.create_zip(zip_path, results_directory)
+        # Remove the results directory to prevent http logs, etc. from getting huge between runs.
+        # We could have create_zip remove the original, but this is more explicit.
+        self._tool.filesystem.rmtree(results_directory)
+        return archive
diff --git a/Tools/Scripts/webkitpy/tool/bot/layouttestresultsreader_unittest.py b/Tools/Scripts/webkitpy/tool/bot/layouttestresultsreader_unittest.py
new file mode 100644
index 0000000..0eb3482
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/layouttestresultsreader_unittest.py
@@ -0,0 +1,105 @@
+# Copyright (c) 2011 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.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.net.layouttestresults import LayoutTestResults
+from webkitpy.tool.bot.layouttestresultsreader import *
+from webkitpy.tool.mocktool import MockTool
+
+
+class LayoutTestResultsReaderTest(unittest.TestCase):
+    def test_missing_layout_test_results(self):
+        tool = MockTool()
+        reader = LayoutTestResultsReader(tool, "/var/logs")
+        layout_tests_results_path = '/mock-results/full_results.json'
+        unit_tests_results_path = '/mock-results/webkit_unit_tests_output.xml'
+        tool.filesystem = MockFileSystem({layout_tests_results_path: None,
+                                          unit_tests_results_path: None})
+        # Make sure that our filesystem mock functions as we expect.
+        self.assertRaises(IOError, tool.filesystem.read_text_file, layout_tests_results_path)
+        self.assertRaises(IOError, tool.filesystem.read_text_file, unit_tests_results_path)
+        # layout_test_results shouldn't raise even if the results.html file is missing.
+        self.assertEquals(reader.results(), None)
+
+    def test_create_unit_test_results(self):
+        tool = MockTool()
+        reader = LayoutTestResultsReader(tool, "/var/logs")
+        unit_tests_results_path = '/mock-results/webkit_unit_tests_output.xml'
+        no_failures_xml = """<?xml version="1.0" encoding="UTF-8"?>
+<testsuites tests="3" failures="0" disabled="0" errors="0" time="11.35" name="AllTests">
+  <testsuite name="RenderTableCellDeathTest" tests="3" failures="0" disabled="0" errors="0" time="0.677">
+    <testcase name="CanSetColumn" status="run" time="0.168" classname="RenderTableCellDeathTest" />
+    <testcase name="CrashIfSettingUnsetColumnIndex" status="run" time="0.129" classname="RenderTableCellDeathTest" />
+    <testcase name="CrashIfSettingUnsetRowIndex" status="run" time="0.123" classname="RenderTableCellDeathTest" />
+  </testsuite>
+</testsuites>"""
+        tool.filesystem = MockFileSystem({unit_tests_results_path: no_failures_xml})
+        self.assertEquals(reader._create_unit_test_results(), [])
+
+    def test_missing_unit_test_results_path(self):
+        tool = MockTool()
+        tool.port().unit_tests_results_path = lambda: None
+        reader = LayoutTestResultsReader(tool, "/var/logs")
+        reader._create_layout_test_results = lambda: LayoutTestResults([])
+        # layout_test_results shouldn't raise even if the unit tests xml file is missing.
+        self.assertNotEquals(reader.results(), None)
+        self.assertEquals(reader.results().failing_tests(), [])
+
+
+    def test_layout_test_results(self):
+        reader = LayoutTestResultsReader(MockTool(), "/var/logs")
+        reader._read_file_contents = lambda path: None
+        self.assertEquals(reader.results(), None)
+        reader._read_file_contents = lambda path: ""
+        self.assertEquals(reader.results(), None)
+        reader._create_layout_test_results = lambda: LayoutTestResults([])
+        results = reader.results()
+        self.assertNotEquals(results, None)
+        self.assertEquals(results.failure_limit_count(), 30)  # This value matches RunTests.NON_INTERACTIVE_FAILURE_LIMIT_COUNT
+
+    def test_archive_last_layout_test_results(self):
+        tool = MockTool()
+        reader = LayoutTestResultsReader(tool, "/var/logs")
+        patch = tool.bugs.fetch_attachment(10001)
+        tool.filesystem = MockFileSystem()
+        # Should fail because the results_directory does not exist.
+        expected_stderr = "/mock-results does not exist, not archiving.\n"
+        archive = OutputCapture().assert_outputs(self, reader.archive, [patch], expected_stderr=expected_stderr)
+        self.assertEqual(archive, None)
+
+        results_directory = "/mock-results"
+        # Sanity check what we assume our mock results directory is.
+        self.assertEqual(reader._results_directory(), results_directory)
+        tool.filesystem.maybe_make_directory(results_directory)
+        self.assertTrue(tool.filesystem.exists(results_directory))
+
+        self.assertNotEqual(reader.archive(patch), None)
+        self.assertFalse(tool.filesystem.exists(results_directory))
diff --git a/Tools/Scripts/webkitpy/tool/bot/patchanalysistask.py b/Tools/Scripts/webkitpy/tool/bot/patchanalysistask.py
new file mode 100644
index 0000000..cde1c84
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/patchanalysistask.py
@@ -0,0 +1,253 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.net.layouttestresults import LayoutTestResults
+
+
+class UnableToApplyPatch(Exception):
+    def __init__(self, patch):
+        Exception.__init__(self)
+        self.patch = patch
+
+
+class PatchAnalysisTaskDelegate(object):
+    def parent_command(self):
+        raise NotImplementedError("subclasses must implement")
+
+    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 expected_failures(self):
+        raise NotImplementedError("subclasses must implement")
+
+    def test_results(self):
+        raise NotImplementedError("subclasses must implement")
+
+    def archive_last_test_results(self, patch):
+        raise NotImplementedError("subclasses must implement")
+
+    def build_style(self):
+        raise NotImplementedError("subclasses must implement")
+
+    # We could make results_archive optional, but for now it's required.
+    def report_flaky_tests(self, patch, flaky_tests, results_archive):
+        raise NotImplementedError("subclasses must implement")
+
+
+class PatchAnalysisTask(object):
+    def __init__(self, delegate, patch):
+        self._delegate = delegate
+        self._patch = patch
+        self._script_error = None
+        self._results_archive_from_patch_test_run = None
+        self._results_from_patch_test_run = None
+        self._expected_failures = delegate.expected_failures()
+
+    def _run_command(self, command, success_message, failure_message):
+        try:
+            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._delegate.command_failed(failure_message, script_error=self._script_error, patch=self._patch)
+            return False
+
+    def _clean(self):
+        return self._run_command([
+            "clean",
+        ],
+        "Cleaned working directory",
+        "Unable to clean working directory")
+
+    def _update(self):
+        # FIXME: Ideally the status server log message should include which revision we updated to.
+        return self._run_command([
+            "update",
+        ],
+        "Updated working directory",
+        "Unable to update working directory")
+
+    def _apply(self):
+        return self._run_command([
+            "apply-attachment",
+            "--no-update",
+            "--non-interactive",
+            self._patch.id(),
+        ],
+        "Applied patch",
+        "Patch does not apply")
+
+    def _build(self):
+        return self._run_command([
+            "build",
+            "--no-clean",
+            "--no-update",
+            "--build-style=%s" % self._delegate.build_style(),
+        ],
+        "Built patch",
+        "Patch does not build")
+
+    def _build_without_patch(self):
+        return self._run_command([
+            "build",
+            "--force-clean",
+            "--no-update",
+            "--build-style=%s" % self._delegate.build_style(),
+        ],
+        "Able to build without patch",
+        "Unable to build without patch")
+
+    def _test(self):
+        return self._run_command([
+            "build-and-test",
+            "--no-clean",
+            "--no-update",
+            # Notice that we don't pass --build, which means we won't build!
+            "--test",
+            "--non-interactive",
+        ],
+        "Passed tests",
+        "Patch does not pass tests")
+
+    def _build_and_test_without_patch(self):
+        return self._run_command([
+            "build-and-test",
+            "--force-clean",
+            "--no-update",
+            "--build",
+            "--test",
+            "--non-interactive",
+        ],
+        "Able to pass tests without patch",
+        "Unable to pass tests without patch (tree is red?)")
+
+    def _land(self):
+        # Unclear if this should pass --quiet or not.  If --parent-command always does the reporting, then it should.
+        return self._run_command([
+            "land-attachment",
+            "--force-clean",
+            "--non-interactive",
+            "--parent-command=" + self._delegate.parent_command(),
+            self._patch.id(),
+        ],
+        "Landed patch",
+        "Unable to land patch")
+
+    def _report_flaky_tests(self, flaky_test_results, results_archive):
+        self._delegate.report_flaky_tests(self._patch, flaky_test_results, results_archive)
+
+    def _results_failed_different_tests(self, first, second):
+        first_failing_tests = [] if not first else first.failing_tests()
+        second_failing_tests = [] if not second else second.failing_tests()
+        return first_failing_tests != second_failing_tests
+
+    def _test_patch(self):
+        if self._test():
+            return True
+
+        # Note: archive_last_test_results deletes the results directory, making these calls order-sensitve.
+        # We could remove this dependency by building the test_results from the archive.
+        first_results = self._delegate.test_results()
+        first_results_archive = self._delegate.archive_last_test_results(self._patch)
+        first_script_error = self._script_error
+        first_failure_status_id = self.failure_status_id
+
+        if self._expected_failures.failures_were_expected(first_results):
+            return True
+
+        if self._test():
+            # Only report flaky tests if we were successful at parsing results.html and archiving results.
+            if first_results and first_results_archive:
+                self._report_flaky_tests(first_results.failing_test_results(), first_results_archive)
+            return True
+
+        second_results = self._delegate.test_results()
+        if self._results_failed_different_tests(first_results, second_results):
+            # We could report flaky tests here, but we would need to be careful
+            # to use similar checks to ExpectedFailures._can_trust_results
+            # to make sure we don't report constant failures as flakes when
+            # we happen to hit the --exit-after-N-failures limit.
+            # See https://bugs.webkit.org/show_bug.cgi?id=51272
+            return False
+
+        # Archive (and remove) second results so test_results() after
+        # build_and_test_without_patch won't use second results instead of the clean-tree results.
+        second_results_archive = self._delegate.archive_last_test_results(self._patch)
+
+        if self._build_and_test_without_patch():
+            # The error from the previous ._test() run is real, report it.
+            return self.report_failure(first_results_archive, first_results, first_script_error)
+
+        clean_tree_results = self._delegate.test_results()
+        self._expected_failures.update(clean_tree_results)
+
+        # Re-check if the original results are now to be expected to avoid a full re-try.
+        if self._expected_failures.failures_were_expected(first_results):
+            return True
+
+        # Now that we have updated information about failing tests with a clean checkout, we can
+        # tell if our original failures were unexpected and fail the patch if necessary.
+        if self._expected_failures.unexpected_failures_observed(first_results):
+            self.failure_status_id = first_failure_status_id
+            return self.report_failure(first_results_archive, first_results, first_script_error)
+
+        # We don't know what's going on.  The tree is likely very red (beyond our layout-test-results
+        # failure limit), just keep retrying the patch. until someone fixes the tree.
+        return False
+
+    def results_archive_from_patch_test_run(self, patch):
+        assert(self._patch.id() == patch.id())  # PatchAnalysisTask is not currently re-useable.
+        return self._results_archive_from_patch_test_run
+
+    def results_from_patch_test_run(self, patch):
+        assert(self._patch.id() == patch.id())  # PatchAnalysisTask is not currently re-useable.
+        return self._results_from_patch_test_run
+
+    def report_failure(self, results_archive=None, results=None, script_error=None):
+        if not self.validate():
+            return False
+        self._results_archive_from_patch_test_run = results_archive
+        self._results_from_patch_test_run = results
+        raise script_error or self._script_error
+
+    def validate(self):
+        raise NotImplementedError("subclasses must implement")
+
+    def run(self):
+        raise NotImplementedError("subclasses must implement")
diff --git a/Tools/Scripts/webkitpy/tool/bot/queueengine.py b/Tools/Scripts/webkitpy/tool/bot/queueengine.py
new file mode 100644
index 0000000..1d75359
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/queueengine.py
@@ -0,0 +1,159 @@
+# Copyright (c) 2009 Google Inc. All rights reserved.
+# Copyright (c) 2009 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:
+# 
+#     * 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 sys
+import traceback
+
+from datetime import datetime, timedelta
+
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.system.deprecated_logging import log, OutputTee
+
+
+# FIXME: This will be caught by "except Exception:" blocks, we should consider
+# making this inherit from SystemExit instead (or BaseException, except that's not recommended).
+class TerminateQueue(Exception):
+    pass
+
+
+class QueueEngineDelegate:
+    def queue_log_path(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def work_item_log_path(self, work_item):
+        raise NotImplementedError, "subclasses must implement"
+
+    def begin_work_queue(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def should_continue_work_queue(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def next_work_item(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def process_work_item(self, work_item):
+        raise NotImplementedError, "subclasses must implement"
+
+    def handle_unexpected_error(self, work_item, message):
+        raise NotImplementedError, "subclasses must implement"
+
+
+class QueueEngine:
+    def __init__(self, name, delegate, wakeup_event):
+        self._name = name
+        self._delegate = delegate
+        self._wakeup_event = wakeup_event
+        self._output_tee = OutputTee()
+
+    log_date_format = "%Y-%m-%d %H:%M:%S"
+    sleep_duration_text = "2 mins"  # This could be generated from seconds_to_sleep
+    seconds_to_sleep = 120
+    handled_error_code = 2
+
+    # Child processes exit with a special code to the parent queue process can detect the error was handled.
+    @classmethod
+    def exit_after_handled_error(cls, error):
+        log(error)
+        sys.exit(cls.handled_error_code)
+
+    def run(self):
+        self._begin_logging()
+
+        self._delegate.begin_work_queue()
+        while (self._delegate.should_continue_work_queue()):
+            try:
+                self._ensure_work_log_closed()
+                work_item = self._delegate.next_work_item()
+                if not work_item:
+                    self._sleep("No work item.")
+                    continue
+
+                # FIXME: Work logs should not depend on bug_id specificaly.
+                #        This looks fixed, no?
+                self._open_work_log(work_item)
+                try:
+                    if not self._delegate.process_work_item(work_item):
+                        log("Unable to process work item.")
+                        continue
+                except ScriptError, e:
+                    # Use a special exit code to indicate that the error was already
+                    # handled in the child process and we should just keep looping.
+                    if e.exit_code == self.handled_error_code:
+                        continue
+                    message = "Unexpected failure when processing patch!  Please file a bug against webkit-patch.\n%s" % e.message_with_output()
+                    self._delegate.handle_unexpected_error(work_item, message)
+            except TerminateQueue, e:
+                self._stopping("TerminateQueue exception received.")
+                return 0
+            except KeyboardInterrupt, e:
+                self._stopping("User terminated queue.")
+                return 1
+            except Exception, e:
+                traceback.print_exc()
+                # Don't try tell the status bot, in case telling it causes an exception.
+                self._sleep("Exception while preparing queue")
+        self._stopping("Delegate terminated queue.")
+        return 0
+
+    def _stopping(self, message):
+        log("\n%s" % message)
+        self._delegate.stop_work_queue(message)
+        # Be careful to shut down our OutputTee or the unit tests will be unhappy.
+        self._ensure_work_log_closed()
+        self._output_tee.remove_log(self._queue_log)
+
+    def _begin_logging(self):
+        self._queue_log = self._output_tee.add_log(self._delegate.queue_log_path())
+        self._work_log = None
+
+    def _open_work_log(self, work_item):
+        work_item_log_path = self._delegate.work_item_log_path(work_item)
+        if not work_item_log_path:
+            return
+        self._work_log = self._output_tee.add_log(work_item_log_path)
+
+    def _ensure_work_log_closed(self):
+        # If we still have a bug log open, close it.
+        if self._work_log:
+            self._output_tee.remove_log(self._work_log)
+            self._work_log = None
+
+    def _now(self):
+        """Overriden by the unit tests to allow testing _sleep_message"""
+        return datetime.now()
+
+    def _sleep_message(self, message):
+        wake_time = self._now() + timedelta(seconds=self.seconds_to_sleep)
+        return "%s Sleeping until %s (%s)." % (message, wake_time.strftime(self.log_date_format), self.sleep_duration_text)
+
+    def _sleep(self, message):
+        log(self._sleep_message(message))
+        self._wakeup_event.wait(self.seconds_to_sleep)
+        self._wakeup_event.clear()
diff --git a/Tools/Scripts/webkitpy/tool/bot/queueengine_unittest.py b/Tools/Scripts/webkitpy/tool/bot/queueengine_unittest.py
new file mode 100644
index 0000000..f959ee1
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/queueengine_unittest.py
@@ -0,0 +1,187 @@
+# Copyright (c) 2009 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 datetime
+import os
+import shutil
+import tempfile
+import threading
+import unittest
+
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.tool.bot.queueengine import QueueEngine, QueueEngineDelegate, TerminateQueue
+
+
+class LoggingDelegate(QueueEngineDelegate):
+    def __init__(self, test):
+        self._test = test
+        self._callbacks = []
+        self._run_before = False
+        self.stop_message = None
+
+    expected_callbacks = [
+        'queue_log_path',
+        'begin_work_queue',
+        'should_continue_work_queue',
+        'next_work_item',
+        'work_item_log_path',
+        'process_work_item',
+        'should_continue_work_queue',
+        'stop_work_queue',
+    ]
+
+    def record(self, method_name):
+        self._callbacks.append(method_name)
+
+    def queue_log_path(self):
+        self.record("queue_log_path")
+        return os.path.join(self._test.temp_dir, "queue_log_path")
+
+    def work_item_log_path(self, work_item):
+        self.record("work_item_log_path")
+        return os.path.join(self._test.temp_dir, "work_log_path", "%s.log" % work_item)
+
+    def begin_work_queue(self):
+        self.record("begin_work_queue")
+
+    def should_continue_work_queue(self):
+        self.record("should_continue_work_queue")
+        if not self._run_before:
+            self._run_before = True
+            return True
+        return False
+
+    def next_work_item(self):
+        self.record("next_work_item")
+        return "work_item"
+
+    def process_work_item(self, work_item):
+        self.record("process_work_item")
+        self._test.assertEquals(work_item, "work_item")
+        return True
+
+    def handle_unexpected_error(self, work_item, message):
+        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
+
+    def process_work_item(self, work_item):
+        self.record("process_work_item")
+        raise self._exception
+
+
+class FastQueueEngine(QueueEngine):
+    def __init__(self, delegate):
+        QueueEngine.__init__(self, "fast-queue", delegate, threading.Event())
+
+    # No sleep for the wicked.
+    seconds_to_sleep = 0
+
+    def _sleep(self, message):
+        pass
+
+
+class QueueEngineTest(unittest.TestCase):
+    def test_trivial(self):
+        delegate = LoggingDelegate(self)
+        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))
+        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
+        # but before any other callback.  Otherwise callbacks should be normal.
+        expected_callbacks.insert(work_item_index + 1, 'handle_unexpected_error')
+        self.assertEquals(delegate._callbacks, expected_callbacks)
+
+    def test_handled_error(self):
+        delegate = RaisingDelegate(self, ScriptError(exit_code=QueueEngine.handled_error_code))
+        self._run_engine(delegate)
+        self.assertEquals(delegate._callbacks, LoggingDelegate.expected_callbacks)
+
+    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.
+        expected_callbacks = LoggingDelegate.expected_callbacks[:work_item_index + 1]
+        expected_callbacks.append("stop_work_queue")
+
+        delegate = RaisingDelegate(self, exception)
+        self._run_engine(delegate, termination_message=termination_message)
+
+        self.assertEquals(delegate._callbacks, expected_callbacks)
+        self.assertEquals(delegate.stop_message, termination_message)
+
+    def test_terminating_error(self):
+        self._test_terminating_queue(KeyboardInterrupt(), "User terminated queue.")
+        self._test_terminating_queue(TerminateQueue(), "TerminateQueue exception received.")
+
+    def test_now(self):
+        """Make sure there are no typos in the QueueEngine.now() method."""
+        engine = QueueEngine("test", None, None)
+        self.assertTrue(isinstance(engine._now(), datetime.datetime))
+
+    def test_sleep_message(self):
+        engine = QueueEngine("test", None, None)
+        engine._now = lambda: datetime.datetime(2010, 1, 1)
+        expected_sleep_message = "MESSAGE Sleeping until 2010-01-01 00:02:00 (2 mins)."
+        self.assertEqual(engine._sleep_message("MESSAGE"), expected_sleep_message)
+
+    def setUp(self):
+        self.temp_dir = tempfile.mkdtemp(suffix="work_queue_test_logs")
+
+    def tearDown(self):
+        shutil.rmtree(self.temp_dir)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/tool/bot/sheriff.py b/Tools/Scripts/webkitpy/tool/bot/sheriff.py
new file mode 100644
index 0000000..a8c928c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/sheriff.py
@@ -0,0 +1,115 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.config import urls
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.tool.grammar import join_with_separators
+
+
+class Sheriff(object):
+    def __init__(self, tool, sheriffbot):
+        self._tool = tool
+        self._sheriffbot = sheriffbot
+
+    def responsible_nicknames_from_commit_info(self, commit_info):
+        nestedList = [party.irc_nicknames for party in commit_info.responsible_parties() if party.irc_nicknames]
+        return reduce(lambda list, childList: list + childList, nestedList)
+
+    def post_irc_warning(self, commit_info, builders):
+        irc_nicknames = sorted(self.responsible_nicknames_from_commit_info(commit_info))
+        irc_prefix = ": " if irc_nicknames else ""
+        irc_message = "%s%s%s might have broken %s" % (
+            ", ".join(irc_nicknames),
+            irc_prefix,
+            urls.view_revision_url(commit_info.revision()),
+            join_with_separators([builder.name() for builder in builders]))
+
+        self._tool.irc().post(irc_message)
+
+    def post_irc_summary(self, failure_map):
+        failing_tests = failure_map.failing_tests()
+        if not failing_tests:
+            return
+        test_list_limit = 5
+        irc_message = "New failures: %s" % ", ".join(sorted(failing_tests)[:test_list_limit])
+        failure_count = len(failing_tests)
+        if failure_count > test_list_limit:
+            irc_message += " (and %s more...)" % (failure_count - test_list_limit)
+        self._tool.irc().post(irc_message)
+
+    def post_rollout_patch(self, svn_revision_list, rollout_reason):
+        # Ensure that svn revisions are numbers (and not options to
+        # create-rollout).
+        try:
+            svn_revisions = " ".join([str(int(revision)) for revision in svn_revision_list])
+        except:
+            raise ScriptError(message="Invalid svn revision number \"%s\"."
+                              % " ".join(svn_revision_list))
+
+        if rollout_reason.startswith("-"):
+            raise ScriptError(message="The rollout reason may not begin "
+                              "with - (\"%s\")." % rollout_reason)
+
+        output = self._sheriffbot.run_webkit_patch([
+            "create-rollout",
+            "--force-clean",
+            # In principle, we should pass --non-interactive here, but it
+            # turns out that create-rollout doesn't need it yet.  We can't
+            # pass it prophylactically because we reject unrecognized command
+            # line switches.
+            "--parent-command=sheriff-bot",
+            svn_revisions,
+            rollout_reason,
+        ])
+        return urls.parse_bug_id(output)
+
+    def post_chromium_deps_roll(self, revision, revision_name):
+        args = [
+            "post-chromium-deps-roll",
+            "--force-clean",
+            "--non-interactive",
+            "--parent-command=sheriff-bot",
+        ]
+        # revision can be None, but revision_name is always something meaningful.
+        args += [revision, revision_name]
+        output = self._sheriffbot.run_webkit_patch(args)
+        return urls.parse_bug_id(output)
+
+    def post_blame_comment_on_bug(self, commit_info, builders, tests):
+        if not commit_info.bug_id():
+            return
+        comment = "%s might have broken %s" % (
+            urls.view_revision_url(commit_info.revision()),
+            join_with_separators([builder.name() for builder in builders]))
+        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)
diff --git a/Tools/Scripts/webkitpy/tool/bot/sheriff_unittest.py b/Tools/Scripts/webkitpy/tool/bot/sheriff_unittest.py
new file mode 100644
index 0000000..3ff5082
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/sheriff_unittest.py
@@ -0,0 +1,89 @@
+# 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 Builder
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.bot.sheriff import Sheriff
+from webkitpy.tool.mocktool import MockTool
+
+
+class MockSheriffBot(object):
+    name = "mock-sheriff-bot"
+    watchers = [
+        "watcher@example.com",
+    ]
+
+    def run_webkit_patch(self, args):
+        return "Created bug https://bugs.webkit.org/show_bug.cgi?id=36936\n"
+
+
+class SheriffTest(unittest.TestCase):
+    def test_post_blame_comment_on_bug(self):
+        def run():
+            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
+            # 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, ["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, ["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']
+--- Begin comment ---
+http://trac.webkit.org/changeset/4321 might have broken Foo and Bar
+--- End comment ---
+
+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 ---
+
+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/Tools/Scripts/webkitpy/tool/bot/stylequeuetask.py b/Tools/Scripts/webkitpy/tool/bot/stylequeuetask.py
new file mode 100644
index 0000000..01f7f72
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/stylequeuetask.py
@@ -0,0 +1,75 @@
+# Copyright (c) 2012 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.bot.patchanalysistask import PatchAnalysisTask, PatchAnalysisTaskDelegate, UnableToApplyPatch
+
+
+class StyleQueueTaskDelegate(PatchAnalysisTaskDelegate):
+    def parent_command(self):
+        return "style-queue"
+
+
+class StyleQueueTask(PatchAnalysisTask):
+    def validate(self):
+        self._patch = self._delegate.refetch_patch(self._patch)
+        if self._patch.is_obsolete():
+            return False
+        if self._patch.bug().is_closed():
+            return False
+        if self._patch.review() == "-":
+            return False
+        return True
+
+    def _check_style(self):
+        return self._run_command([
+            "check-style-local",
+            "--non-interactive",
+            "--quiet",
+        ],
+        "Style checked",
+        "Patch did not pass style check")
+
+    def _apply_watch_list(self):
+        return self._run_command([
+            "apply-watchlist-local",
+            self._patch.bug_id(),
+        ],
+        "Watchlist applied",
+        "Unabled to apply watchlist")
+
+    def run(self):
+        if not self._clean():
+            return False
+        if not self._update():
+            return False
+        if not self._apply():
+            raise UnableToApplyPatch(self._patch)
+        self._apply_watch_list()
+        if not self._check_style():
+            return self.report_failure()
+        return True
diff --git a/Tools/Scripts/webkitpy/tool/commands/__init__.py b/Tools/Scripts/webkitpy/tool/commands/__init__.py
new file mode 100644
index 0000000..4e8eb62
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/__init__.py
@@ -0,0 +1,24 @@
+# Required for Python to search this directory for module files
+
+from webkitpy.tool.commands.adduserstogroups import AddUsersToGroups
+from webkitpy.tool.commands.analyzechangelog import AnalyzeChangeLog
+from webkitpy.tool.commands.applywatchlistlocal import ApplyWatchListLocal
+from webkitpy.tool.commands.bugfortest import BugForTest
+from webkitpy.tool.commands.bugsearch import BugSearch
+from webkitpy.tool.commands.chromechannels import ChromeChannels
+from webkitpy.tool.commands.download import *
+from webkitpy.tool.commands.earlywarningsystem import *
+from webkitpy.tool.commands.expectations import OptimizeExpectations
+from webkitpy.tool.commands.findusers import FindUsers
+from webkitpy.tool.commands.gardenomatic import GardenOMatic
+from webkitpy.tool.commands.openbugs import OpenBugs
+from webkitpy.tool.commands.perfalizer import Perfalizer
+from webkitpy.tool.commands.prettydiff import PrettyDiff
+from webkitpy.tool.commands.queries import *
+from webkitpy.tool.commands.queues import *
+from webkitpy.tool.commands.rebaseline import Rebaseline
+from webkitpy.tool.commands.rebaselineserver import RebaselineServer
+from webkitpy.tool.commands.roll import *
+from webkitpy.tool.commands.sheriffbot import *
+from webkitpy.tool.commands.upload import *
+from webkitpy.tool.commands.suggestnominations import *
diff --git a/Tools/Scripts/webkitpy/tool/commands/abstractlocalservercommand.py b/Tools/Scripts/webkitpy/tool/commands/abstractlocalservercommand.py
new file mode 100644
index 0000000..6c54da2
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/abstractlocalservercommand.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2011 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:
+#
+# 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from optparse import make_option
+import threading
+
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+
+
+class AbstractLocalServerCommand(AbstractDeclarativeCommand):
+    server = None
+    launch_path = "/"
+
+    def __init__(self):
+        options = [
+            make_option("--httpd-port", action="store", type="int", default=8127, help="Port to use for the HTTP server"),
+            make_option("--no-show-results", action="store_false", default=True, dest="show_results", help="Don't launch a browser with the rebaseline server"),
+        ]
+        AbstractDeclarativeCommand.__init__(self, options=options)
+
+    def _prepare_config(self, options, args, tool):
+        return None
+
+    def execute(self, options, args, tool):
+        config = self._prepare_config(options, args, tool)
+
+        server_url = "http://localhost:%d%s" % (options.httpd_port, self.launch_path)
+        print "Starting server at %s" % server_url
+        print "Use the 'Exit' link in the UI, %squitquitquit or Ctrl-C to stop" % server_url
+
+        if options.show_results:
+            # FIXME: This seems racy.
+            threading.Timer(0.1, lambda: self._tool.user.open_url(server_url)).start()
+
+        httpd = self.server(httpd_port=options.httpd_port, config=config)
+        httpd.serve_forever()
diff --git a/Tools/Scripts/webkitpy/tool/commands/abstractsequencedcommand.py b/Tools/Scripts/webkitpy/tool/commands/abstractsequencedcommand.py
new file mode 100644
index 0000000..5eaf249
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/abstractsequencedcommand.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.tool.commands.stepsequence import StepSequence
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+
+
+class AbstractSequencedCommand(AbstractDeclarativeCommand):
+    steps = None
+    def __init__(self):
+        self._sequence = StepSequence(self.steps)
+        AbstractDeclarativeCommand.__init__(self, self._sequence.options())
+
+    def _prepare_state(self, options, args, tool):
+        return None
+
+    def execute(self, options, args, tool):
+        try:
+            state = self._prepare_state(options, args, tool)
+        except ScriptError, e:
+            log(e.message_with_output())
+            self._exit(e.exit_code or 2)
+
+        self._sequence.run_and_handle_errors(tool, options, state)
diff --git a/Tools/Scripts/webkitpy/tool/commands/adduserstogroups.py b/Tools/Scripts/webkitpy/tool/commands/adduserstogroups.py
new file mode 100644
index 0000000..2286958
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/adduserstogroups.py
@@ -0,0 +1,65 @@
+# Copyright (c) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+
+
+class AddUsersToGroups(AbstractDeclarativeCommand):
+    name = "add-users-to-groups"
+    help_text = "Add users matching subtring to specified groups"
+
+    # This probably belongs in bugzilla.py
+    known_groups = ['canconfirm', 'editbugs']
+
+    def execute(self, options, args, tool):
+        search_string = args[0]
+        # FIXME: We could allow users to specify groups on the command line.
+        list_title = 'Add users matching "%s" which groups?' % search_string
+        # FIXME: Need a way to specify that "none" is not allowed.
+        # FIXME: We could lookup what groups the current user is able to grant from bugzilla.
+        groups = tool.user.prompt_with_list(list_title, self.known_groups, can_choose_multiple=True)
+        if not groups:
+            print "No groups specified."
+            return
+
+        login_userid_pairs = tool.bugs.queries.fetch_login_userid_pairs_matching_substring(search_string)
+        if not login_userid_pairs:
+            print "No users found matching '%s'" % search_string
+            return
+
+        print "Found %s users matching %s:" % (len(login_userid_pairs), search_string)
+        for (login, user_id) in login_userid_pairs:
+            print "%s (%s)" % (login, user_id)
+
+        confirm_message = "Are you sure you want add %s users to groups %s?  (This action cannot be undone using webkit-patch.)" % (len(login_userid_pairs), groups)
+        if not tool.user.confirm(confirm_message):
+            return
+
+        for (login, user_id) in login_userid_pairs:
+            print "Adding %s to %s" % (login, groups)
+            tool.bugs.add_user_to_groups(user_id, groups)
diff --git a/Tools/Scripts/webkitpy/tool/commands/analyzechangelog.py b/Tools/Scripts/webkitpy/tool/commands/analyzechangelog.py
new file mode 100644
index 0000000..b88b61f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/analyzechangelog.py
@@ -0,0 +1,206 @@
+# Copyright (c) 2011 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 json
+import re
+import time
+
+from webkitpy.common.checkout.scm.detection import SCMDetector
+from webkitpy.common.checkout.changelog import ChangeLog
+from webkitpy.common.config.contributionareas import ContributionAreas
+from webkitpy.common.system.filesystem import FileSystem
+from webkitpy.common.system.executive import Executive
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+from webkitpy.tool import steps
+
+
+class AnalyzeChangeLog(AbstractDeclarativeCommand):
+    name = "analyze-changelog"
+    help_text = "Experimental command for analyzing change logs."
+    long_help = "This command parses changelogs in a specified directory and summarizes the result as JSON files."
+
+    def __init__(self):
+        options = [
+            steps.Options.changelog_count,
+        ]
+        AbstractDeclarativeCommand.__init__(self, options=options)
+
+    @staticmethod
+    def _enumerate_changelogs(filesystem, dirname, changelog_count):
+        changelogs = [filesystem.join(dirname, filename) for filename in filesystem.listdir(dirname) if re.match('^ChangeLog(-(\d{4}-\d{2}-\d{2}))?$', filename)]
+        # Make sure ChangeLog shows up before ChangeLog-2011-01-01
+        changelogs = sorted(changelogs, key=lambda filename: filename + 'X', reverse=True)
+        return changelogs[:changelog_count]
+
+    @staticmethod
+    def _generate_jsons(filesystem, jsons, output_dir):
+        for filename in jsons:
+            print '    Generating', filename
+            filesystem.write_text_file(filesystem.join(output_dir, filename), json.dumps(jsons[filename], indent=2))
+
+    def execute(self, options, args, tool):
+        filesystem = self._tool.filesystem
+        if len(args) < 1 or not filesystem.exists(args[0]):
+            print "Need the directory name to look for changelog as the first argument"
+            return
+        changelog_dir = filesystem.abspath(args[0])
+
+        if len(args) < 2 or not filesystem.exists(args[1]):
+            print "Need the output directory name as the second argument"
+            return
+        output_dir = args[1]
+
+        startTime = time.time()
+
+        print 'Enumerating ChangeLog files...'
+        changelogs = AnalyzeChangeLog._enumerate_changelogs(filesystem, changelog_dir, options.changelog_count)
+
+        analyzer = ChangeLogAnalyzer(tool, changelogs)
+        analyzer.analyze()
+
+        print 'Generating json files...'
+        json_files = {
+            'summary.json': analyzer.summary(),
+            'contributors.json': analyzer.contributors_statistics(),
+            'areas.json': analyzer.areas_statistics(),
+        }
+        AnalyzeChangeLog._generate_jsons(filesystem, json_files, output_dir)
+        commands_dir = filesystem.dirname(filesystem.path_to_module(self.__module__))
+        print commands_dir
+        filesystem.copyfile(filesystem.join(commands_dir, 'data/summary.html'), filesystem.join(output_dir, 'summary.html'))
+
+        tick = time.time() - startTime
+        print 'Finished in %02dm:%02ds' % (int(tick / 60), int(tick % 60))
+
+
+class ChangeLogAnalyzer(object):
+    def __init__(self, host, changelog_paths):
+        self._changelog_paths = changelog_paths
+        self._filesystem = host.filesystem
+        self._contribution_areas = ContributionAreas(host.filesystem)
+        self._scm = host.scm()
+        self._parsed_revisions = {}
+
+        self._contributors_statistics = {}
+        self._areas_statistics = dict([(area, {'reviewed': 0, 'unreviewed': 0, 'contributors': {}}) for area in self._contribution_areas.names()])
+        self._summary = {'reviewed': 0, 'unreviewed': 0}
+
+        self._longest_filename = max([len(path) - len(self._scm.checkout_root) for path in changelog_paths])
+        self._filename = ''
+        self._length_of_previous_output = 0
+
+    def contributors_statistics(self):
+        return self._contributors_statistics
+
+    def areas_statistics(self):
+        return self._areas_statistics
+
+    def summary(self):
+        return self._summary
+
+    def _print_status(self, status):
+        if self._length_of_previous_output:
+            print "\r" + " " * self._length_of_previous_output,
+        new_output = ('%' + str(self._longest_filename) + 's: %s') % (self._filename, status)
+        print "\r" + new_output,
+        self._length_of_previous_output = len(new_output)
+
+    def _set_filename(self, filename):
+        if self._filename:
+            print
+        self._filename = filename
+
+    def analyze(self):
+        for path in self._changelog_paths:
+            self._set_filename(self._filesystem.relpath(path, self._scm.checkout_root))
+            with self._filesystem.open_text_file_for_reading(path) as changelog:
+                self._print_status('Parsing entries...')
+                number_of_parsed_entries = self._analyze_entries(ChangeLog.parse_entries_from_file(changelog), path)
+            self._print_status('Done (%d entries)' % number_of_parsed_entries)
+        print
+        self._summary['contributors'] = len(self._contributors_statistics)
+        self._summary['contributors_with_reviews'] = sum([1 for contributor in self._contributors_statistics.values() if contributor['reviews']['total']])
+        self._summary['contributors_without_reviews'] = self._summary['contributors'] - self._summary['contributors_with_reviews']
+
+    def _collect_statistics_for_contributor_area(self, area, contributor, contribution_type, reviewed):
+        area_contributors = self._areas_statistics[area]['contributors']
+        if contributor not in area_contributors:
+            area_contributors[contributor] = {'reviews': 0, 'reviewed': 0, 'unreviewed': 0}
+        if contribution_type == 'patches':
+            contribution_type = 'reviewed' if reviewed else 'unreviewed'
+        area_contributors[contributor][contribution_type] += 1
+
+    def _collect_statistics_for_contributor(self, contributor, contribution_type, areas, touched_files, reviewed):
+        if contributor not in self._contributors_statistics:
+            self._contributors_statistics[contributor] = {
+                'reviews': {'total': 0, 'areas': {}, 'files': {}},
+                'patches': {'reviewed': 0, 'unreviewed': 0, 'areas': {}, 'files': {}}}
+        statistics = self._contributors_statistics[contributor][contribution_type]
+
+        if contribution_type == 'reviews':
+            statistics['total'] += 1
+        elif reviewed:
+            statistics['reviewed'] += 1
+        else:
+            statistics['unreviewed'] += 1
+
+        for area in areas:
+            self._increment_dictionary_value(statistics['areas'], area)
+            self._collect_statistics_for_contributor_area(area, contributor, contribution_type, reviewed)
+        for touchedfile in touched_files:
+            self._increment_dictionary_value(statistics['files'], touchedfile)
+
+    def _increment_dictionary_value(self, dictionary, key):
+        dictionary[key] = dictionary.get(key, 0) + 1
+
+    def _analyze_entries(self, entries, changelog_path):
+        dirname = self._filesystem.dirname(changelog_path)
+        for i, entry in enumerate(entries):
+            self._print_status('(%s) entries' % i)
+            assert(entry.authors())
+
+            touchedfiles_for_entry = [self._filesystem.relpath(self._filesystem.join(dirname, name), self._scm.checkout_root) for name in entry.touched_files()]
+            areas_for_entry = self._contribution_areas.areas_for_touched_files(touchedfiles_for_entry)
+            authors_for_entry = entry.authors()
+            reviewers_for_entry = entry.reviewers()
+
+            for reviewer in reviewers_for_entry:
+                self._collect_statistics_for_contributor(reviewer.full_name, 'reviews', areas_for_entry, touchedfiles_for_entry, reviewed=True)
+
+            for author in authors_for_entry:
+                self._collect_statistics_for_contributor(author['name'], 'patches', areas_for_entry, touchedfiles_for_entry,
+                    reviewed=bool(reviewers_for_entry))
+
+            for area in areas_for_entry:
+                self._areas_statistics[area]['reviewed' if reviewers_for_entry else 'unreviewed'] += 1
+
+            self._summary['reviewed' if reviewers_for_entry else 'unreviewed'] += 1
+
+            i += 1
+        self._print_status('(%s) entries' % i)
+        return i
diff --git a/Tools/Scripts/webkitpy/tool/commands/analyzechangelog_unittest.py b/Tools/Scripts/webkitpy/tool/commands/analyzechangelog_unittest.py
new file mode 100644
index 0000000..661d2d8
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/analyzechangelog_unittest.py
@@ -0,0 +1,185 @@
+# Cpyright (c) 2011 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 json
+import sys
+from webkitpy.common.config.contributionareas import ContributionAreas
+from webkitpy.common.host_mock import MockHost
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.executive import Executive
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.tool.commands.analyzechangelog import AnalyzeChangeLog
+from webkitpy.tool.commands.analyzechangelog import ChangeLogAnalyzer
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+
+
+class AnalyzeChangeLogTest(CommandsTest):
+    def test_enumerate_enumerate_changelogs(self):
+        filesystem = MockFileSystem({
+            'foo/ChangeLog': '',
+            'foo/ChangeLog-2010-06-23': '',
+            'foo/ChangeLog-2010-12-31': '',
+            'foo/ChangeLog-x': '',
+            'foo/ChangeLog-2011-01-01': '',
+        })
+        changelogs = AnalyzeChangeLog._enumerate_changelogs(filesystem, 'foo/', None)
+        self.assertEqual(changelogs, ['foo/ChangeLog', 'foo/ChangeLog-2011-01-01', 'foo/ChangeLog-2010-12-31', 'foo/ChangeLog-2010-06-23'])
+
+        changelogs = AnalyzeChangeLog._enumerate_changelogs(filesystem, 'foo/', 2)
+        self.assertEqual(changelogs, ['foo/ChangeLog', 'foo/ChangeLog-2011-01-01'])
+
+    def test_generate_jsons(self):
+        filesystem = MockFileSystem()
+        test_json = {'array.json': [1, 2, 3, {'key': 'value'}], 'dictionary.json': {'somekey': 'somevalue', 'array': [4, 5]}}
+
+        capture = OutputCapture()
+        capture.capture_output()
+
+        AnalyzeChangeLog._generate_jsons(filesystem, test_json, 'bar')
+        self.assertEqual(set(filesystem.files.keys()), set(['bar/array.json', 'bar/dictionary.json']))
+
+        capture.restore_output()
+
+        self.assertEqual(json.loads(filesystem.files['bar/array.json']), test_json['array.json'])
+        self.assertEqual(json.loads(filesystem.files['bar/dictionary.json']), test_json['dictionary.json'])
+
+
+class ChangeLogAnalyzerTest(CommandsTest):
+    def test_analyze_one_changelog(self):
+        host = MockHost()
+        host.filesystem.files['mock-checkout/foo/ChangeLog'] = u"""2011-11-17  Mark Rowe  <mrowe@apple.com>
+
+    <http://webkit.org/b/72646> Disable deprecation warnings around code where we cannot easily
+    switch away from the deprecated APIs.
+
+    Reviewed by Sam Weinig.
+
+    * platform/mac/WebCoreNSStringExtras.mm:
+    * platform/network/cf/SocketStreamHandleCFNet.cpp:
+    (WebCore::SocketStreamHandle::reportErrorToClient):
+
+2011-11-19  Kevin Ollivier  <kevino@theolliviers.com>
+
+    [wx] C++ bindings build fix for move of array classes to WTF.
+
+    * bindings/scripts/CodeGeneratorCPP.pm:
+    (GetCPPTypeGetter):
+    (GetNamespaceForClass):
+    (GenerateHeader):
+    (GenerateImplementation):
+
+2011-10-27  Philippe Normand  <pnormand@igalia.com> and Zan Dobersek  <zandobersek@gmail.com>
+
+        [GStreamer] WebAudio AudioFileReader implementation
+        https://bugs.webkit.org/show_bug.cgi?id=69834
+
+        Reviewed by Martin Robinson.
+
+        Basic FileReader implementation, supporting one or 2 audio
+        channels. An empty AudioDestination is also provided, its complete
+        implementation is handled in bug 69835.
+
+        * GNUmakefile.am:
+        * GNUmakefile.list.am:
+        * platform/audio/gstreamer/AudioDestinationGStreamer.cpp: Added.
+        (WebCore::AudioDestination::create):
+        (WebCore::AudioDestination::hardwareSampleRate):
+        (WebCore::AudioDestinationGStreamer::AudioDestinationGStreamer):
+        (WebCore::AudioDestinationGStreamer::~AudioDestinationGStreamer):
+        (WebCore::AudioDestinationGStreamer::start):
+        (WebCore::AudioDestinationGStreamer::stop):
+        * platform/audio/gstreamer/AudioDestinationGStreamer.h: Added.
+        (WebCore::AudioDestinationGStreamer::isPlaying):
+        (WebCore::AudioDestinationGStreamer::sampleRate):
+        (WebCore::AudioDestinationGStreamer::sourceProvider):
+        * platform/audio/gstreamer/AudioFileReaderGStreamer.cpp: Added.
+        (WebCore::getGStreamerAudioCaps):
+        (WebCore::getFloatFromByteReader):
+        (WebCore::copyGstreamerBuffersToAudioChannel):
+        (WebCore::onAppsinkNewBufferCallback):
+        (WebCore::messageCallback):
+        (WebCore::onGStreamerDeinterleavePadAddedCallback):
+        (WebCore::onGStreamerDeinterleaveReadyCallback):
+        (WebCore::onGStreamerDecodebinPadAddedCallback):
+        (WebCore::AudioFileReader::AudioFileReader):
+        (WebCore::AudioFileReader::~AudioFileReader):
+        (WebCore::AudioFileReader::handleBuffer):
+        (WebCore::AudioFileReader::handleMessage):
+        (WebCore::AudioFileReader::handleNewDeinterleavePad):
+        (WebCore::AudioFileReader::deinterleavePadsConfigured):
+        (WebCore::AudioFileReader::plugDeinterleave):
+        (WebCore::AudioFileReader::createBus):
+        (WebCore::createBusFromAudioFile):
+        (WebCore::createBusFromInMemoryAudioFile):
+        * platform/audio/gtk/AudioBusGtk.cpp: Added.
+        (WebCore::AudioBus::loadPlatformResource):
+"""
+
+        capture = OutputCapture()
+        capture.capture_output()
+
+        analyzer = ChangeLogAnalyzer(host, ['mock-checkout/foo/ChangeLog'])
+        analyzer.analyze()
+
+        capture.restore_output()
+
+        self.assertEqual(analyzer.summary(),
+            {'reviewed': 2, 'unreviewed': 1, 'contributors': 6, 'contributors_with_reviews': 2, 'contributors_without_reviews': 4})
+
+        self.assertEqual(set(analyzer.contributors_statistics().keys()),
+            set(['Sam Weinig', u'Mark Rowe', u'Kevin Ollivier', 'Martin Robinson', u'Philippe Normand', u'Zan Dobersek']))
+
+        self.assertEqual(analyzer.contributors_statistics()['Sam Weinig'],
+            {'reviews': {'files': {u'foo/platform/mac/WebCoreNSStringExtras.mm': 1, u'foo/platform/network/cf/SocketStreamHandleCFNet.cpp': 1},
+            'total': 1, 'areas': {'Network': 1}}, 'patches': {'files': {}, 'areas': {}, 'unreviewed': 0, 'reviewed': 0}})
+        self.assertEqual(analyzer.contributors_statistics()[u'Mark Rowe'],
+            {'reviews': {'files': {}, 'total': 0, 'areas': {}}, 'patches': {'files': {u'foo/platform/mac/WebCoreNSStringExtras.mm': 1,
+            u'foo/platform/network/cf/SocketStreamHandleCFNet.cpp': 1}, 'areas': {'Network': 1}, 'unreviewed': 0, 'reviewed': 1}})
+        self.assertEqual(analyzer.contributors_statistics()[u'Kevin Ollivier'],
+            {'reviews': {'files': {}, 'total': 0, 'areas': {}}, 'patches': {'files': {u'foo/bindings/scripts/CodeGeneratorCPP.pm': 1},
+                'areas': {'Bindings': 1}, 'unreviewed': 1, 'reviewed': 0}})
+
+        files_for_audio_patch = {u'foo/GNUmakefile.am': 1, u'foo/GNUmakefile.list.am': 1, 'foo/platform/audio/gstreamer/AudioDestinationGStreamer.cpp': 1,
+            'foo/platform/audio/gstreamer/AudioDestinationGStreamer.h': 1, 'foo/platform/audio/gstreamer/AudioFileReaderGStreamer.cpp': 1,
+            'foo/platform/audio/gtk/AudioBusGtk.cpp': 1}
+        author_expectation_for_audio_patch = {'reviews': {'files': {}, 'total': 0, 'areas': {}},
+            'patches': {'files': files_for_audio_patch, 'areas': {'The WebKitGTK+ Port': 1}, 'unreviewed': 0, 'reviewed': 1}}
+        self.assertEqual(analyzer.contributors_statistics()[u'Martin Robinson'],
+            {'reviews': {'files': files_for_audio_patch, 'total': 1, 'areas': {'The WebKitGTK+ Port': 1}},
+                'patches': {'files': {}, 'areas': {}, 'unreviewed': 0, 'reviewed': 0}})
+        self.assertEqual(analyzer.contributors_statistics()[u'Philippe Normand'], author_expectation_for_audio_patch)
+        self.assertEqual(analyzer.contributors_statistics()[u'Zan Dobersek'], author_expectation_for_audio_patch)
+
+        areas_statistics = analyzer.areas_statistics()
+        areas_with_patches = [area for area in areas_statistics if areas_statistics[area]['reviewed'] or areas_statistics[area]['unreviewed']]
+        self.assertEqual(set(areas_with_patches), set(['Bindings', 'Network', 'The WebKitGTK+ Port']))
+        self.assertEqual(areas_statistics['Bindings'], {'unreviewed': 1, 'reviewed': 0, 'contributors':
+            {u'Kevin Ollivier': {'reviews': 0, 'unreviewed': 1, 'reviewed': 0}}})
+        self.assertEqual(areas_statistics['Network'], {'unreviewed': 0, 'reviewed': 1, 'contributors':
+            {'Sam Weinig': {'reviews': 1, 'unreviewed': 0, 'reviewed': 0}, u'Mark Rowe': {'reviews': 0, 'unreviewed': 0, 'reviewed': 1}}})
diff --git a/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal.py b/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal.py
new file mode 100644
index 0000000..6735d48
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal.py
@@ -0,0 +1,50 @@
+# Copyright (c) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand
+from webkitpy.tool import steps
+
+
+class ApplyWatchListLocal(AbstractSequencedCommand):
+    name = "apply-watchlist-local"
+    help_text = "Applies the watchlist to local changes"
+    argument_names = "[BUGID]"
+    steps = [
+        steps.ApplyWatchList,
+    ]
+    long_help = """"Applies the watchlist to local changes.
+The results is logged if a bug is no given. This may be used to try out a watchlist change."""
+
+    def _prepare_state(self, options, args, tool):
+        if len(args) > 1:
+            raise Exception("Too many arguments given: %s" % (' '.join(args)))
+        if not args:
+            return {}
+        return {
+            "bug_id": args[0],
+        }
diff --git a/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal_unittest.py b/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal_unittest.py
new file mode 100644
index 0000000..91818d1
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal_unittest.py
@@ -0,0 +1,50 @@
+# Copyright (c) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.commands.applywatchlistlocal import ApplyWatchListLocal
+
+
+class ApplyWatchListLocalTest(CommandsTest):
+    def test_args_parsing(self):
+        expected_stderr = 'MockWatchList: determine_cc_and_messages\n'
+        self.assert_execute_outputs(ApplyWatchListLocal(), [''], expected_stderr=expected_stderr)
+
+    def test_args_parsing_with_bug(self):
+        expected_stderr = """MockWatchList: determine_cc_and_messages
+MOCK bug comment: bug_id=50002, cc=set(['eric@webkit.org', 'levin@chromium.org', 'abarth@webkit.org'])
+--- Begin comment ---
+Message1.
+
+Message2.
+--- End comment ---\n\n"""
+        self.assert_execute_outputs(ApplyWatchListLocal(), ['50002'], expected_stderr=expected_stderr)
+
+    def test_args_parsing_with_two_bugs(self):
+        self._assertRaisesRegexp(Exception, 'Too many arguments given: 1234 5678', self.assert_execute_outputs, ApplyWatchListLocal(), ['1234', '5678'])
diff --git a/Tools/Scripts/webkitpy/tool/commands/bugfortest.py b/Tools/Scripts/webkitpy/tool/commands/bugfortest.py
new file mode 100644
index 0000000..36aa6b5
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/bugfortest.py
@@ -0,0 +1,48 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+from webkitpy.tool.bot.flakytestreporter import FlakyTestReporter
+
+
+# This is mostly a command for testing FlakyTestReporter, however
+# it could be easily expanded to auto-create bugs, etc. if another
+# command outside of webkitpy wanted to use it.
+class BugForTest(AbstractDeclarativeCommand):
+    name = "bug-for-test"
+    help_text = "Finds the bugzilla bug for a given test"
+
+    def execute(self, options, args, tool):
+        reporter = FlakyTestReporter(tool, "webkitpy")
+        search_string = args[0]
+        bug = reporter._lookup_bug_for_flaky_test(search_string)
+        if bug:
+            bug = reporter._follow_duplicate_chain(bug)
+            print "%5s %s" % (bug.id(), bug.title())
+        else:
+            print "No bugs found matching '%s'" % search_string
diff --git a/Tools/Scripts/webkitpy/tool/commands/bugsearch.py b/Tools/Scripts/webkitpy/tool/commands/bugsearch.py
new file mode 100644
index 0000000..a1d74c5
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/bugsearch.py
@@ -0,0 +1,49 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+
+
+class BugSearch(AbstractDeclarativeCommand):
+    name = "bug-search"
+    help_text = "List bugs matching a query"
+    argument_names = "QUERY"
+    long_help = \
+"""Runs the bugzilla quicksearch QUERY on bugs.webkit.org, and lists all bugs
+returned. QUERY can be as simple as a bug number or a comma delimited list of
+bug numbers.
+See https://bugzilla.mozilla.org/page.cgi?id=quicksearch.html for full
+documentation on the query format."""
+
+    def execute(self, options, args, tool):
+        search_string = args[0]
+        bugs = tool.bugs.queries.fetch_bugs_matching_quicksearch(search_string)
+        for bug in bugs:
+            print "%5s %s" % (bug.id(), bug.title())
+        if not bugs:
+            print "No bugs found matching '%s'" % search_string
diff --git a/Tools/Scripts/webkitpy/tool/commands/chromechannels.py b/Tools/Scripts/webkitpy/tool/commands/chromechannels.py
new file mode 100644
index 0000000..da093b4
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/chromechannels.py
@@ -0,0 +1,104 @@
+# Copyright (c) 2012 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from optparse import make_option
+
+from webkitpy.common.net.omahaproxy import OmahaProxy
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+
+import re
+
+
+class ChromeChannels(AbstractDeclarativeCommand):
+    name = "chrome-channels"
+    help_text = "List which chrome channels include the patches in bugs returned by QUERY."
+    argument_names = "QUERY"
+    long_help = """Retrieves the current list of Chrome releases from omahaproxy.appspot.com,
+and then runs the bugzilla quicksearch QUERY on bugs.bugzilla.org. For each bug
+returned by query, a single svn commit is deduced, and a short summary is
+printed of each bug listing which Chrome channels contain each bugs associated
+commit.
+
+The QUERY can be as simple as a bug number, or a comma delimited list of bug
+numbers. See https://bugzilla.mozilla.org/page.cgi?id=quicksearch.html for full
+documentation on the query format."""
+
+    chrome_channels = OmahaProxy.chrome_channels
+    commited_pattern = "Committed r([0-9]+): <http://trac.webkit.org/changeset/\\1>"
+    rollout_pattern = "Rolled out in http://trac.webkit.org/changeset/[0-9]+"
+
+    def __init__(self):
+        AbstractDeclarativeCommand.__init__(self)
+        self._re_committed = re.compile(self.commited_pattern)
+        self._re_rollout = re.compile(self.rollout_pattern)
+        self._omahaproxy = OmahaProxy()
+
+    def _channels_for_bug(self, revisions, bug):
+        comments = bug.comments()
+        commit = None
+
+        # Scan the comments, looking for a sane list of commits and rollbacks.
+        for comment in comments:
+            commit_match = self._re_committed.search(comment['text'])
+            if commit_match:
+                if commit:
+                    return "%5s %s\n... has too confusing a commit history to parse, skipping\n" % (bug.id(), bug.title())
+                commit = int(commit_match.group(1))
+            if self._re_rollout.search(comment['text']):
+                commit = None
+        if not commit:
+            return "%5s %s\n... does not appear to have an associated commit.\n" % (bug.id(), bug.title())
+
+        # We now know that we have a commit, so gather up the list of platforms
+        # by channel, then print.
+        by_channel = {}
+        for revision in revisions:
+            channel = revision['channel']
+            if revision['commit'] < commit:
+                continue
+            if not channel in by_channel:
+                by_channel[revision['channel']] = " %6s:" % channel
+            by_channel[channel] += " %s," % revision['platform']
+        if not by_channel:
+            return "%5s %s (r%d)\n... not yet released in any Chrome channels.\n" % (bug.id(), bug.title(), commit)
+        retval = "%5s %s (r%d)\n" % (bug.id(), bug.title(), commit)
+        for channel in self.chrome_channels:
+            if channel in by_channel:
+                retval += by_channel[channel][:-1]
+                retval += "\n"
+        return retval
+
+    def execute(self, options, args, tool):
+        search_string = args[0]
+        revisions = self._omahaproxy.get_revisions()
+        bugs = tool.bugs.queries.fetch_bugs_matching_quicksearch(search_string)
+        if not bugs:
+            print "No bugs found matching '%s'" % search_string
+            return
+        for bug in bugs:
+            print self._channels_for_bug(revisions, bug),
diff --git a/Tools/Scripts/webkitpy/tool/commands/chromechannels_unittest.py b/Tools/Scripts/webkitpy/tool/commands/chromechannels_unittest.py
new file mode 100644
index 0000000..037aebb
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/chromechannels_unittest.py
@@ -0,0 +1,99 @@
+# Copyright (c) 2012 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.commands.chromechannels import ChromeChannels
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.mocktool import MockTool
+from webkitpy.common.net.omahaproxy import OmahaProxy
+
+
+class MockOmahaProxy(OmahaProxy):
+    revisions = [{"commit": 20, "channel": "canary", "platform": "Mac", "date": "07/04/76"},
+                 {"commit": 20, "channel": "canary", "platform": "Windows", "date": "07/04/76"},
+                 {"commit": 25, "channel": "dev", "platform": "Mac", "date": "07/01/76"},
+                 {"commit": 30, "channel": "dev", "platform": "Windows", "date": "03/29/82"},
+                 {"commit": 30, "channel": "dev", "platform": "Linux", "date": "03/29/82"},
+                 {"commit": 15, "channel": "beta", "platform": "Windows", "date": "07/04/67"},
+                 {"commit": 15, "channel": "beta", "platform": "Linux", "date": "07/04/67"},
+                 {"commit": 10, "channel": "stable", "platform": "Windows", "date": "07/01/67"},
+                 {"commit": 20, "channel": "stable", "platform": "Linux", "date": "09/16/10"},
+                 ]
+
+    def get_revisions(self):
+        return self.revisions
+
+
+class TestableChromeChannels(ChromeChannels):
+    def __init__(self):
+        ChromeChannels.__init__(self)
+        self._omahaproxy = MockOmahaProxy()
+
+
+class ChromeChannelsTest(CommandsTest):
+
+    single_bug_expectations = {
+        50001: """50001 Bug with a patch needing review. (r35)
+... not yet released in any Chrome channels.
+""",
+        50002: """50002 The third bug
+... has too confusing a commit history to parse, skipping
+""",
+        50003: """50003 The fourth bug
+... does not appear to have an associated commit.
+""",
+        50004: """50004 The fifth bug (r15)
+ canary: Mac, Windows
+    dev: Mac, Windows, Linux
+   beta: Windows, Linux
+ stable: Linux
+"""}
+
+    def test_single_bug(self):
+        testable_chrome_channels = TestableChromeChannels()
+        tool = MockTool()
+        testable_chrome_channels.bind_to_tool(tool)
+        revisions = testable_chrome_channels._omahaproxy.get_revisions()
+        for bug_id, expectation in self.single_bug_expectations.items():
+            self.assertEqual(testable_chrome_channels._channels_for_bug(revisions, testable_chrome_channels._tool.bugs.fetch_bug(bug_id)),
+                             expectation)
+
+    def test_with_query(self):
+        expected_stdout = \
+"""50001 Bug with a patch needing review. (r35)
+... not yet released in any Chrome channels.
+50002 The third bug
+... has too confusing a commit history to parse, skipping
+50003 The fourth bug
+... does not appear to have an associated commit.
+50004 The fifth bug (r15)
+ canary: Mac, Windows
+    dev: Mac, Windows, Linux
+   beta: Windows, Linux
+ stable: Linux
+"""
+        self.assert_execute_outputs(TestableChromeChannels(), ["foo"], expected_stdout=expected_stdout)
diff --git a/Tools/Scripts/webkitpy/tool/commands/commandtest.py b/Tools/Scripts/webkitpy/tool/commands/commandtest.py
new file mode 100644
index 0000000..eea0a61
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/commandtest.py
@@ -0,0 +1,48 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.webkitunittest import TestCase
+from webkitpy.tool.mocktool import MockOptions, MockTool
+
+
+class CommandsTest(TestCase):
+    def assert_execute_outputs(self, command, args=[], expected_stdout="", expected_stderr="", expected_exception=None, options=MockOptions(), tool=MockTool()):
+        options.blocks = None
+        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, expected_exception=expected_exception)
diff --git a/Tools/Scripts/webkitpy/tool/commands/data/summary.html b/Tools/Scripts/webkitpy/tool/commands/data/summary.html
new file mode 100644
index 0000000..abf80d8
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/data/summary.html
@@ -0,0 +1,455 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>ChangeLog Analysis</title>
+<style type="text/css">
+
+body {
+    font-family: 'Helvetica' 'Segoe UI Light' sans-serif;
+    font-weight: 200;
+    padding: 20px;
+    min-width: 1200px;
+}
+
+* {
+    padding: 0px;
+    margin: 0px;
+    border: 0px;
+}
+
+h1, h2, h3 {
+    font-weight: 200;
+}
+
+h1 {
+    margin: 0 0 1em 0;
+}
+
+h2 {
+    font-size: 1.2em;
+    text-align: center;
+    margin-bottom: 1em;
+}
+
+h3 {
+    font-size: 1em;
+}
+
+.view {
+    margin: 0px;
+    width: 600px;
+    float: left;
+}
+
+.graph-container p {
+    width: 200px;
+    text-align: right;
+    margin: 20px 0 20px 0;
+    padding: 5px;
+    border-right: solid 1px black;
+}
+
+.graph-container table {
+    width: 100%;
+}
+
+.graph-container table, .graph-container td {
+    border-collapse: collapse;
+    border: none;
+}
+
+.graph-container td {
+    padding: 5px;
+    vertical-align: center;
+}
+
+.graph-container td:first-child {
+    width: 200px;
+    text-align: right;
+    border-right: solid 1px black;
+}
+
+.graph-container .selected {
+    background: #eee;
+}
+
+#reviewers .selected td:first-child {
+    border-radius: 10px 0px 0px 10px;
+}
+
+#areas .selected td:last-child {
+    border-radius: 0px 10px 10px 0px;
+}
+
+.graph-container .bar {
+    display: inline-block;
+    min-height: 1em;
+    background: #9f6;
+    margin-right: 0.4ex;
+}
+
+.graph-container .reviewed-patches {
+    background: #3cf;
+    margin-right: 1px;
+}
+
+.graph-container .unreviewed-patches {
+    background: #f99;
+}
+
+.constrained {
+    background: #eee;
+    border-radius: 10px;
+}
+
+.constrained .vertical-bar {
+    border-right: solid 1px #eee;
+}
+
+#header {
+    border-spacing: 5px;
+}
+
+#header section {
+    display: table-cell;
+    width: 200px;
+    vertical-align: top;
+    border: solid 2px #ccc;
+    border-collapse: collapse;
+    padding: 5px;
+    font-size: 0.8em;
+}
+
+#header dt {
+    float: left;
+}
+
+#header dt:after {
+    content: ': ';
+}
+
+#header .legend {
+    width: 600px;
+}
+
+.legend .bar {
+    width: 15ex;
+    padding: 2px;
+}
+
+.legend .reviews {
+    width: 25ex;
+}
+
+.legend td:first-child {
+    width: 18ex;
+}
+
+</style>
+</head>
+<body>
+<h1>ChangeLog Analysis</h1>
+
+<section id="header">
+<section id="summary">
+<h2>Summary</h2>
+</section>
+
+<section class="legend">
+<h2>Legend</h2>
+<div class="graph-container">
+<table>
+<tbody>
+<tr><td>Contributor's name</td>
+<td><span class="bar reviews">Reviews</span> <span class="value-container">(# of reviews)</span><br>
+<span class="bar reviewed-patches">Reviewed</span><span class="bar unreviewed-patches">Unreviewed</span>
+<span class="value-container">(# of reviewed):(# of unreviewed)</span></td></tr>
+</tbody>
+</table>
+</div>
+</section>
+</section>
+
+<section id="contributors" class="view">
+<h2 id="contributors-title">Contributors</h2>
+<div class="graph-container"></div>
+</section>
+
+<section id="areas" class="view">
+<h2 id="areas-title">Areas of contributions</h2>
+<div class="graph-container"></div>
+</section>
+
+<script>
+
+// Naive implementation of element extensions discussed on public-webapps
+
+if (!Element.prototype.append) {
+    Element.prototype.append = function () {
+        for (var i = 0; i < arguments.length; i++) {
+            // FIXME: Take care of other node types
+            if (arguments[i] instanceof Element || arguments[i] instanceof CharacterData)
+                this.appendChild(arguments[i]);
+            else
+                this.appendChild(document.createTextNode(arguments[i]));
+        }
+        return this;
+    }
+}
+
+if (!Node.prototype.remove) {
+    Node.prototype.remove = function () {
+        this.parentNode.removeChild(this);
+        return this;
+    }
+}
+
+if (!Element.create) {
+    Element.create = function () {
+        if (arguments.length < 1)
+            return null;
+        var element = document.createElement(arguments[0]);
+        if (arguments.length == 1)
+            return element;
+
+        // FIXME: the second argument can be content or IDL attributes
+        var attributes = arguments[1];
+        for (attribute in attributes)
+            element.setAttribute(attribute, attributes[attribute]);
+
+        if (arguments.length >= 3)
+            element.append.apply(element, arguments[2]);
+
+        return element;
+    }
+}
+
+if (!Node.prototype.removeAllChildren) {
+    Node.prototype.removeAllChildren = function () {
+        while (this.firstChild)
+            this.firstChild.remove();
+        return this;
+    }
+}
+
+Element.prototype.removeClassNameFromAllElements = function (className) {
+    var elements = this.getElementsByClassName(className);
+    for (var i = 0; i < elements.length; i++)
+        elements[i].classList.remove(className);
+}
+
+function getJSON(url, callback) {
+    var xhr = new XMLHttpRequest();
+    xhr.open('GET', url, true);
+    xhr.onreadystatechange = function () {
+        if (this.readyState == 4)
+            callback(JSON.parse(xhr.responseText));
+    }
+    xhr.send();
+}
+
+function GraphView(container) {
+    this._container = container;
+    this._defaultData = null;
+}
+
+GraphView.prototype.setData = function(data, constrained) {
+    if (constrained)
+        this._container.classList.add('constrained');
+    else
+        this._container.classList.remove('constrained');
+    this._clearGraph();
+    this._constructGraph(data);
+}
+
+GraphView.prototype.setDefaultData = function(data) {
+    this._defaultData = data;
+    this.setData(data);
+}
+
+GraphView.prototype.reset = function () {
+    this.setMarginTop();
+    this.setData(this._defaultData);
+}
+
+GraphView.prototype.isConstrained = function () { return this._container.classList.contains('constrained'); }
+
+GraphView.prototype.targetRow = function (node) {
+    var target = null;
+
+    while (node && node != this._container) {
+        if (node.localName == 'tr')
+            target = node;
+        node = node.parentNode;
+    }
+
+    return node && target;
+}
+
+GraphView.prototype.selectRow = function (row) {
+    this._container.removeClassNameFromAllElements('selected');
+    row.classList.add('selected');
+}
+
+GraphView.prototype.setMarginTop = function (y) { this._container.style.marginTop = y ? y + 'px' : null; }
+GraphView.prototype._graphContainer = function () { return this._container.getElementsByClassName('graph-container')[0]; }
+GraphView.prototype._clearGraph = function () { return this._graphContainer().removeAllChildren(); }
+
+GraphView.prototype._numberOfPatches = function (dataItem) {
+    return dataItem.numberOfReviewedPatches + (dataItem.numberOfUnreviewedPatches !== undefined ? dataItem.numberOfUnreviewedPatches : 0);
+}
+
+GraphView.prototype._maximumValue = function (labels, data) {
+    var numberOfPatches = this._numberOfPatches;
+    return Math.max.apply(null, labels.map(function (label) {
+        return Math.max(numberOfPatches(data[label]), data[label].numberOfReviews !== undefined ? data[label].numberOfReviews : 0);
+    }));
+}
+
+GraphView.prototype._sortLabelsByNumberOfReviwsAndReviewedPatches = function(data) {
+    var labels = Object.keys(data);
+    if (!labels.length)
+        return null;
+    var numberOfPatches = this._numberOfPatches;
+    var computeValue = function (dataItem) {
+        return numberOfPatches(dataItem) + (dataItem.numberOfReviews !== undefined ? dataItem.numberOfReviews : 0);
+    }
+    labels.sort(function (a, b) { return computeValue(data[b]) - computeValue(data[a]); });
+    return labels;
+}
+
+GraphView.prototype._constructGraph = function (data) {
+    var element = this._graphContainer();
+    var labels = this._sortLabelsByNumberOfReviwsAndReviewedPatches(data);
+    if (!labels) {
+        element.append(Element.create('p', {}, ['None']));
+        return;
+    }
+
+    var maxValue = this._maximumValue(labels, data);
+    var computeStyleForBar = function (value) { return 'width:' + (value * 85.0 / maxValue) + '%' }
+
+    var table = Element.create('table', {}, [Element.create('tbody')]);
+    for (var i = 0; i < labels.length; i++) {
+        var label = labels[i];
+        var item = data[label];
+        var row = Element.create('tr', {}, [Element.create('td', {}, [label]), Element.create('td', {})]);
+        var valueCell = row.lastChild;
+
+        if (item.numberOfReviews != undefined) {
+            valueCell.append(
+                Element.create('span', {'class': 'bar reviews', 'style': computeStyleForBar(item.numberOfReviews) }),
+                Element.create('span', {'class': 'value-container'}, [item.numberOfReviews]),
+                Element.create('br')
+            );
+        }
+
+        valueCell.append(Element.create('span', {'class': 'bar reviewed-patches', 'style': computeStyleForBar(item.numberOfReviewedPatches) }));
+        if (item.numberOfUnreviewedPatches !== undefined)
+            valueCell.append(Element.create('span', {'class': 'bar unreviewed-patches', 'style': computeStyleForBar(item.numberOfUnreviewedPatches) }));
+
+        valueCell.append(Element.create('span', {'class': 'value-container'},
+            [item.numberOfReviewedPatches + (item.numberOfUnreviewedPatches !== undefined ? ':' + item.numberOfUnreviewedPatches : '')]));
+
+        table.firstChild.append(row);
+        row.label = label;
+        row.data = item;
+    }
+    element.append(table);
+}
+
+var contributorsView = new GraphView(document.querySelector('#contributors'));
+var areasView = new GraphView(document.querySelector('#areas'));
+
+getJSON('summary.json',
+    function (summary) {
+        var summaryContainer = document.querySelector('#summary');
+        summaryContainer.append(Element.create('dl', {}, [
+            Element.create('dt', {}, ['Total entries (reviewed)']),
+            Element.create('dd', {}, [(summary['reviewed'] + summary['unreviewed']) + ' (' + summary['reviewed'] + ')']),
+            Element.create('dt', {}, ['Total contributors']),
+            Element.create('dd', {}, [summary['contributors']]),
+            Element.create('dt', {}, ['Contributors who reviewed']),
+            Element.create('dd', {}, [summary['contributors_with_reviews']]),
+        ]));
+    });
+
+getJSON('contributors.json',
+    function (contributors) {
+        for (var contributor in contributors) {
+            contributor = contributors[contributor];
+            contributor.numberOfReviews = contributor.reviews ? contributor.reviews.total : 0;
+            contributor.numberOfReviewedPatches = contributor.patches ? contributor.patches.reviewed : 0;
+            contributor.numberOfUnreviewedPatches = contributor.patches ? contributor.patches.unreviewed : 0;
+        }
+        contributorsView.setDefaultData(contributors);
+    });
+
+getJSON('areas.json',
+    function (areas) {
+        for (var area in areas) {
+            areas[area].numberOfReviewedPatches = areas[area].reviewed;
+            areas[area].numberOfUnreviewedPatches = areas[area].unreviewed;
+        }
+        areasView.setDefaultData(areas);
+    });
+
+function contributorAreas(contributorData) {
+    var areas = new Object;
+    for (var area in contributorData.reviews.areas) {
+        if (!areas[area])
+            areas[area] = {'numberOfReviewedPatches': 0};
+        areas[area].numberOfReviews = contributorData.reviews.areas[area];
+    }
+    for (var area in contributorData.patches.areas) {
+        if (!areas[area])
+            areas[area] = {'numberOfReviews': 0};
+        areas[area].numberOfReviewedPatches = contributorData.patches.areas[area];
+    }
+    return areas;
+}
+
+function areaContributors(areaData) {
+    var contributors = areaData['contributors'];
+    for (var contributor in contributors) {
+        contributor = contributors[contributor];
+        contributor.numberOfReviews = contributor.reviews;
+        contributor.numberOfReviewedPatches = contributor.reviewed;
+        contributor.numberOfUnreviewedPatches = contributor.unreviewed;
+    }
+    return contributors;
+}
+
+var mouseTimer = 0;
+window.onmouseover = function (event) {
+    clearTimeout(mouseTimer);
+
+    var row = contributorsView.targetRow(event.target);
+    if (row) {
+        if (!contributorsView.isConstrained()) {
+            contributorsView.selectRow(row);
+            areasView.setMarginTop(row.firstChild.offsetTop);
+            areasView.setData(contributorAreas(row.data), 'constrained');
+        }
+        return;
+    }
+
+    row = areasView.targetRow(event.target);
+    if (row) {
+        if (!areasView.isConstrained()) {
+            areasView.selectRow(row);
+            contributorsView.setMarginTop(row.firstChild.offsetTop);
+            contributorsView.setData(areaContributors(row.data), 'constrained');
+        }
+        return;
+    }
+
+    mouseTimer = setTimeout(function () {
+        contributorsView.reset();
+        areasView.reset();
+    }, 500);
+}
+
+</script>
+</body>
+</html>
diff --git a/Tools/Scripts/webkitpy/tool/commands/download.py b/Tools/Scripts/webkitpy/tool/commands/download.py
new file mode 100644
index 0000000..1f73d18
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/download.py
@@ -0,0 +1,477 @@
+# Copyright (c) 2009, 2011 Google Inc. All rights reserved.
+# Copyright (c) 2009 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:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool import steps
+
+from webkitpy.common.checkout.changelog import ChangeLog
+from webkitpy.common.config import urls
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand
+from webkitpy.tool.commands.stepsequence import StepSequence
+from webkitpy.tool.comments import bug_comment_from_commit_text
+from webkitpy.tool.grammar import pluralize
+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)"
+    steps = [
+        steps.CleanWorkingDirectory,
+        steps.Update,
+    ]
+
+
+class Build(AbstractSequencedCommand):
+    name = "build"
+    help_text = "Update working copy and build"
+    steps = [
+        steps.CleanWorkingDirectory,
+        steps.Update,
+        steps.Build,
+    ]
+
+    def _prepare_state(self, options, args, tool):
+        options.build = True
+
+
+class BuildAndTest(AbstractSequencedCommand):
+    name = "build-and-test"
+    help_text = "Update working copy, build, and run the tests"
+    steps = [
+        steps.CleanWorkingDirectory,
+        steps.Update,
+        steps.Build,
+        steps.RunTests,
+    ]
+
+
+class Land(AbstractSequencedCommand):
+    name = "land"
+    help_text = "Land the current working directory diff and updates the associated bug if any"
+    argument_names = "[BUGID]"
+    show_in_main_help = True
+    steps = [
+        steps.AddSvnMimetypeForPng,
+        steps.UpdateChangeLogsWithReviewer,
+        steps.ValidateReviewer,
+        steps.ValidateChangeLogs, # We do this after UpdateChangeLogsWithReviewer to avoid not having to cache the diff twice.
+        steps.Build,
+        steps.RunTests,
+        steps.Commit,
+        steps.CloseBugForLandDiff,
+    ]
+    long_help = """land commits the current working copy diff (just as svn or git commit would).
+land will NOT build and run the tests before committing, but you can use the --build option for that.
+If a bug id is provided, or one can be found in the ChangeLog land will update the bug after committing."""
+
+    def _prepare_state(self, options, args, tool):
+        changed_files = self._tool.scm().changed_files(options.git_commit)
+        return {
+            "changed_files": changed_files,
+            "bug_id": (args and args[0]) or tool.checkout().bug_id_for_this_commit(options.git_commit, changed_files),
+        }
+
+
+class LandCowboy(AbstractSequencedCommand):
+    name = "land-cowboy"
+    help_text = "Prepares a ChangeLog and lands the current working directory diff."
+    steps = [
+        steps.PrepareChangeLog,
+        steps.EditChangeLog,
+        steps.CheckStyle,
+        steps.ConfirmDiff,
+        steps.Build,
+        steps.RunTests,
+        steps.Commit,
+        steps.CloseBugForLandDiff,
+    ]
+
+    def _prepare_state(self, options, args, tool):
+        options.check_style_filter = "-changelog"
+
+
+class LandCowhand(LandCowboy):
+    # Gender-blind term for cowboy, see: http://en.wiktionary.org/wiki/cowhand
+    name = "land-cowhand"
+
+
+class CheckStyleLocal(AbstractSequencedCommand):
+    name = "check-style-local"
+    help_text = "Run check-webkit-style on the current working directory diff"
+    steps = [
+        steps.CheckStyle,
+    ]
+
+
+class AbstractPatchProcessingCommand(AbstractDeclarativeCommand):
+    # Subclasses must implement the methods below.  We don't declare them here
+    # because we want to be able to implement them with mix-ins.
+    #
+    # def _fetch_list_of_patches_to_process(self, options, args, tool):
+    # def _prepare_to_process(self, options, args, tool):
+
+    @staticmethod
+    def _collect_patches_by_bug(patches):
+        bugs_to_patches = {}
+        for patch in patches:
+            bugs_to_patches[patch.bug_id()] = bugs_to_patches.get(patch.bug_id(), []) + [patch]
+        return bugs_to_patches
+
+    def execute(self, options, args, tool):
+        self._prepare_to_process(options, args, tool)
+        patches = self._fetch_list_of_patches_to_process(options, args, tool)
+
+        # It's nice to print out total statistics.
+        bugs_to_patches = self._collect_patches_by_bug(patches)
+        log("Processing %s from %s." % (pluralize("patch", len(patches)), pluralize("bug", len(bugs_to_patches))))
+
+        for patch in patches:
+            self._process_patch(patch, options, args, tool)
+
+
+class AbstractPatchSequencingCommand(AbstractPatchProcessingCommand):
+    prepare_steps = None
+    main_steps = None
+
+    def __init__(self):
+        options = []
+        self._prepare_sequence = StepSequence(self.prepare_steps)
+        self._main_sequence = StepSequence(self.main_steps)
+        options = sorted(set(self._prepare_sequence.options() + self._main_sequence.options()))
+        AbstractPatchProcessingCommand.__init__(self, options)
+
+    def _prepare_to_process(self, options, args, tool):
+        self._prepare_sequence.run_and_handle_errors(tool, options)
+
+    def _process_patch(self, patch, options, args, tool):
+        state = { "patch" : patch }
+        self._main_sequence.run_and_handle_errors(tool, options, state)
+
+
+class ProcessAttachmentsMixin(object):
+    def _fetch_list_of_patches_to_process(self, options, args, tool):
+        return map(lambda patch_id: tool.bugs.fetch_attachment(patch_id), args)
+
+
+class ProcessBugsMixin(object):
+    def _fetch_list_of_patches_to_process(self, options, args, tool):
+        all_patches = []
+        for bug_id in args:
+            patches = tool.bugs.fetch_bug(bug_id).reviewed_patches()
+            log("%s found on bug %s." % (pluralize("reviewed patch", len(patches)), bug_id))
+            all_patches += patches
+        if not all_patches:
+            log("No reviewed patches found, looking for unreviewed patches.")
+            for bug_id in args:
+                patches = tool.bugs.fetch_bug(bug_id).patches()
+                log("%s found on bug %s." % (pluralize("patch", len(patches)), bug_id))
+                all_patches += patches
+        return all_patches
+
+
+class ProcessURLsMixin(object):
+    def _fetch_list_of_patches_to_process(self, options, args, tool):
+        all_patches = []
+        for url in args:
+            bug_id = urls.parse_bug_id(url)
+            if bug_id:
+                patches = tool.bugs.fetch_bug(bug_id).patches()
+                log("%s found on bug %s." % (pluralize("patch", len(patches)), bug_id))
+                all_patches += patches
+
+            attachment_id = urls.parse_attachment_id(url)
+            if attachment_id:
+                all_patches += tool.bugs.fetch_attachment(attachment_id)
+
+        return all_patches
+
+
+class CheckStyle(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
+    name = "check-style"
+    help_text = "Run check-webkit-style on the specified attachments"
+    argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+    main_steps = [
+        steps.CleanWorkingDirectory,
+        steps.Update,
+        steps.ApplyPatch,
+        steps.CheckStyle,
+    ]
+
+
+class BuildAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
+    name = "build-attachment"
+    help_text = "Apply and build patches from bugzilla"
+    argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+    main_steps = [
+        steps.CleanWorkingDirectory,
+        steps.Update,
+        steps.ApplyPatch,
+        steps.Build,
+    ]
+
+
+class BuildAndTestAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
+    name = "build-and-test-attachment"
+    help_text = "Apply, build, and test patches from bugzilla"
+    argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+    main_steps = [
+        steps.CleanWorkingDirectory,
+        steps.Update,
+        steps.ApplyPatch,
+        steps.Build,
+        steps.RunTests,
+    ]
+
+
+class AbstractPatchApplyingCommand(AbstractPatchSequencingCommand):
+    prepare_steps = [
+        steps.EnsureLocalCommitIfNeeded,
+        steps.CleanWorkingDirectoryWithLocalCommits,
+        steps.Update,
+    ]
+    main_steps = [
+        steps.ApplyPatchWithLocalCommit,
+    ]
+    long_help = """Updates the working copy.
+Downloads and applies the patches, creating local commits if necessary."""
+
+
+class ApplyAttachment(AbstractPatchApplyingCommand, ProcessAttachmentsMixin):
+    name = "apply-attachment"
+    help_text = "Apply an attachment to the local working directory"
+    argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+    show_in_main_help = True
+
+
+class ApplyFromBug(AbstractPatchApplyingCommand, ProcessBugsMixin):
+    name = "apply-from-bug"
+    help_text = "Apply reviewed patches from provided bugs to the local working directory"
+    argument_names = "BUGID [BUGIDS]"
+    show_in_main_help = True
+
+
+class ApplyWatchList(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
+    name = "apply-watchlist"
+    help_text = "Applies the watchlist to the specified attachments"
+    argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+    main_steps = [
+        steps.CleanWorkingDirectory,
+        steps.Update,
+        steps.ApplyPatch,
+        steps.ApplyWatchList,
+    ]
+    long_help = """"Applies the watchlist to the specified attachments.
+Downloads the attachment, applies it locally, runs the watchlist against it, and updates the bug with the result."""
+
+
+class AbstractPatchLandingCommand(AbstractPatchSequencingCommand):
+    main_steps = [
+        steps.CleanWorkingDirectory,
+        steps.Update,
+        steps.ApplyPatch,
+        steps.ValidateChangeLogs,
+        steps.ValidateReviewer,
+        steps.Build,
+        steps.RunTests,
+        steps.Commit,
+        steps.ClosePatch,
+        steps.CloseBug,
+    ]
+    long_help = """Checks to make sure builders are green.
+Updates the working copy.
+Applies the patch.
+Builds.
+Runs the layout tests.
+Commits the patch.
+Clears the flags on the patch.
+Closes the bug if no patches are marked for review."""
+
+
+class LandAttachment(AbstractPatchLandingCommand, ProcessAttachmentsMixin):
+    name = "land-attachment"
+    help_text = "Land patches from bugzilla, optionally building and testing them first"
+    argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+    show_in_main_help = True
+
+
+class LandFromBug(AbstractPatchLandingCommand, ProcessBugsMixin):
+    name = "land-from-bug"
+    help_text = "Land all patches on the given bugs, optionally building and testing them first"
+    argument_names = "BUGID [BUGIDS]"
+    show_in_main_help = True
+
+
+class LandFromURL(AbstractPatchLandingCommand, ProcessURLsMixin):
+    name = "land-from-url"
+    help_text = "Land all patches on the given URLs, optionally building and testing them first"
+    argument_names = "URL [URLS]"
+
+
+class ValidateChangelog(AbstractSequencedCommand):
+    name = "validate-changelog"
+    help_text = "Validate that the ChangeLogs and reviewers look reasonable"
+    long_help = """Examines the current diff to see whether the ChangeLogs
+and the reviewers listed in the ChangeLogs look reasonable.
+"""
+    steps = [
+        steps.ValidateChangeLogs,
+        steps.ValidateReviewer,
+    ]
+
+
+class AbstractRolloutPrepCommand(AbstractSequencedCommand):
+    argument_names = "REVISION [REVISIONS] REASON"
+
+    def _commit_info(self, revision):
+        commit_info = self._tool.checkout().commit_info_for_revision(revision)
+        if commit_info and commit_info.bug_id():
+            # Note: Don't print a bug URL here because it will confuse the
+            #       SheriffBot because the SheriffBot just greps the output
+            #       of create-rollout for bug URLs.  It should do better
+            #       parsing instead.
+            log("Preparing rollout for bug %s." % commit_info.bug_id())
+        else:
+            log("Unable to parse bug number from diff.")
+        return commit_info
+
+    def _prepare_state(self, options, args, tool):
+        revision_list = []
+        for revision in str(args[0]).split():
+            if revision.isdigit():
+                revision_list.append(int(revision))
+            else:
+                raise ScriptError(message="Invalid svn revision number: " + revision)
+        revision_list.sort()
+
+        # We use the earliest revision for the bug info
+        earliest_revision = revision_list[0]
+        state = {
+            "revision": earliest_revision,
+            "revision_list": revision_list,
+            "reason": args[1],
+        }
+        commit_info = self._commit_info(earliest_revision)
+        if commit_info:
+            state["bug_id"] = commit_info.bug_id()
+            cc_list = sorted([party.bugzilla_email()
+                            for party in commit_info.responsible_parties()
+                            if party.bugzilla_email()])
+            # FIXME: We should used the list as the canonical representation.
+            state["bug_cc"] = ",".join(cc_list)
+        return state
+
+
+class PrepareRollout(AbstractRolloutPrepCommand):
+    name = "prepare-rollout"
+    help_text = "Revert the given revision(s) in the working copy and prepare ChangeLogs with revert reason"
+    long_help = """Updates the working copy.
+Applies the inverse diff for the provided revision(s).
+Creates an appropriate rollout ChangeLog, including a trac link and bug link.
+"""
+    steps = [
+        steps.CleanWorkingDirectory,
+        steps.Update,
+        steps.RevertRevision,
+        steps.PrepareChangeLogForRevert,
+    ]
+
+
+class CreateRollout(AbstractRolloutPrepCommand):
+    name = "create-rollout"
+    help_text = "Creates a bug to track the broken SVN revision(s) and uploads a rollout patch."
+    steps = [
+        steps.CleanWorkingDirectory,
+        steps.Update,
+        steps.RevertRevision,
+        steps.CreateBug,
+        steps.PrepareChangeLogForRevert,
+        steps.PostDiffForRevert,
+    ]
+
+    def _prepare_state(self, options, args, tool):
+        state = AbstractRolloutPrepCommand._prepare_state(self, options, args, tool)
+        # Currently, state["bug_id"] points to the bug that caused the
+        # regression.  We want to create a new bug that blocks the old bug
+        # so we move state["bug_id"] to state["bug_blocked"] and delete the
+        # old state["bug_id"] so that steps.CreateBug will actually create
+        # the new bug that we want (and subsequently store its bug id into
+        # state["bug_id"])
+        state["bug_blocked"] = state["bug_id"]
+        del state["bug_id"]
+        state["bug_title"] = "REGRESSION(r%s): %s" % (state["revision"], state["reason"])
+        state["bug_description"] = "%s broke the build:\n%s" % (urls.view_revision_url(state["revision"]), state["reason"])
+        # FIXME: If we had more context here, we could link to other open bugs
+        #        that mention the test that regressed.
+        if options.parent_command == "sheriff-bot":
+            state["bug_description"] += """
+
+This is an automatic bug report generated by the sheriff-bot. If this bug
+report was created because of a flaky test, please file a bug for the flaky
+test (if we don't already have one on file) and dup this bug against that bug
+so that we can track how often these flaky tests case pain.
+
+"Only you can prevent forest fires." -- Smokey the Bear
+"""
+        return state
+
+
+class Rollout(AbstractRolloutPrepCommand):
+    name = "rollout"
+    show_in_main_help = True
+    help_text = "Revert the given revision(s) in the working copy and optionally commit the revert and re-open the original bug"
+    long_help = """Updates the working copy.
+Applies the inverse diff for the provided revision.
+Creates an appropriate rollout ChangeLog, including a trac link and bug link.
+Opens the generated ChangeLogs in $EDITOR.
+Shows the prepared diff for confirmation.
+Commits the revert and updates the bug (including re-opening the bug if necessary)."""
+    steps = [
+        steps.CleanWorkingDirectory,
+        steps.Update,
+        steps.RevertRevision,
+        steps.PrepareChangeLogForRevert,
+        steps.EditChangeLog,
+        steps.ConfirmDiff,
+        steps.Build,
+        steps.Commit,
+        steps.ReopenBugAfterRollout,
+    ]
diff --git a/Tools/Scripts/webkitpy/tool/commands/download_unittest.py b/Tools/Scripts/webkitpy/tool/commands/download_unittest.py
new file mode 100644
index 0000000..b71f3da
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/download_unittest.py
@@ -0,0 +1,316 @@
+# Copyright (C) 2009, 2011 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.system.outputcapture import OutputCapture
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.commands.download import *
+from webkitpy.tool.mocktool import MockOptions, MockTool
+from webkitpy.common.checkout.checkout_mock import MockCheckout
+
+
+class AbstractRolloutPrepCommandTest(unittest.TestCase):
+    def test_commit_info(self):
+        command = AbstractRolloutPrepCommand()
+        tool = MockTool()
+        command.bind_to_tool(tool)
+        output = OutputCapture()
+
+        expected_stderr = "Preparing rollout for bug 50000.\n"
+        commit_info = output.assert_outputs(self, command._commit_info, [1234], expected_stderr=expected_stderr)
+        self.assertTrue(commit_info)
+
+        mock_commit_info = Mock()
+        mock_commit_info.bug_id = lambda: None
+        tool._checkout.commit_info_for_revision = lambda revision: mock_commit_info
+        expected_stderr = "Unable to parse bug number from diff.\n"
+        commit_info = output.assert_outputs(self, command._commit_info, [1234], expected_stderr=expected_stderr)
+        self.assertEqual(commit_info, mock_commit_info)
+
+    def test_prepare_state(self):
+        command = AbstractRolloutPrepCommand()
+        mock_commit_info = MockCheckout().commit_info_for_revision(123)
+        command._commit_info = lambda revision: mock_commit_info
+
+        state = command._prepare_state(None, ["124 123 125", "Reason"], None)
+        self.assertEqual(123, state["revision"])
+        self.assertEqual([123, 124, 125], state["revision_list"])
+
+        self.assertRaises(ScriptError, command._prepare_state, options=None, args=["125 r122  123", "Reason"], tool=None)
+        self.assertRaises(ScriptError, command._prepare_state, options=None, args=["125 foo 123", "Reason"], tool=None)
+
+        command._commit_info = lambda revision: None
+        state = command._prepare_state(None, ["124 123 125", "Reason"], None)
+        self.assertEqual(123, state["revision"])
+        self.assertEqual([123, 124, 125], state["revision_list"])
+
+
+class DownloadCommandsTest(CommandsTest):
+    def _default_options(self):
+        options = MockOptions()
+        options.build = True
+        options.build_style = True
+        options.check_style = True
+        options.check_style_filter = None
+        options.clean = True
+        options.close_bug = True
+        options.force_clean = False
+        options.non_interactive = False
+        options.parent_command = 'MOCK parent command'
+        options.quiet = False
+        options.test = True
+        options.update = True
+        return options
+
+    def test_build(self):
+        expected_stderr = "Updating working directory\nBuilding WebKit\n"
+        self.assert_execute_outputs(Build(), [], options=self._default_options(), expected_stderr=expected_stderr)
+
+    def test_build_and_test(self):
+        expected_stderr = "Updating working directory\nBuilding WebKit\nRunning Python unit tests\nRunning Perl unit tests\nRunning JavaScriptCore tests\nRunning WebKit unit tests\nRunning run-webkit-tests\n"
+        self.assert_execute_outputs(BuildAndTest(), [], options=self._default_options(), expected_stderr=expected_stderr)
+
+    def test_apply_attachment(self):
+        options = self._default_options()
+        options.update = True
+        options.local_commit = True
+        expected_stderr = "Updating working directory\nProcessing 1 patch from 1 bug.\nProcessing patch 10000 from bug 50000.\n"
+        self.assert_execute_outputs(ApplyAttachment(), [10000], options=options, expected_stderr=expected_stderr)
+
+    def test_apply_from_bug(self):
+        options = self._default_options()
+        options.update = True
+        options.local_commit = True
+
+        expected_stderr = "Updating working directory\n0 reviewed patches found on bug 50001.\nNo reviewed patches found, looking for unreviewed patches.\n1 patch found on bug 50001.\nProcessing 1 patch from 1 bug.\nProcessing patch 10002 from bug 50001.\n"
+        self.assert_execute_outputs(ApplyFromBug(), [50001], options=options, expected_stderr=expected_stderr)
+
+        expected_stderr = "Updating working directory\n2 reviewed patches found on bug 50000.\nProcessing 2 patches from 1 bug.\nProcessing patch 10000 from bug 50000.\nProcessing patch 10001 from bug 50000.\n"
+        self.assert_execute_outputs(ApplyFromBug(), [50000], options=options, expected_stderr=expected_stderr)
+
+    def test_apply_watch_list(self):
+        expected_stderr = """Processing 1 patch from 1 bug.
+Updating working directory
+MOCK run_and_throw_if_fail: ['mock-update-webkit'], cwd=/mock-checkout\nProcessing patch 10000 from bug 50000.
+MockWatchList: determine_cc_and_messages
+"""
+        self.assert_execute_outputs(ApplyWatchList(), [10000], options=self._default_options(), expected_stderr=expected_stderr, tool=MockTool(log_executive=True))
+
+    def test_land(self):
+        expected_stderr = "Building WebKit\nRunning Python unit tests\nRunning Perl unit tests\nRunning JavaScriptCore tests\nRunning WebKit unit tests\nRunning run-webkit-tests\nCommitted r49824: <http://trac.webkit.org/changeset/49824>\nUpdating bug 50000\n"
+        mock_tool = MockTool()
+        mock_tool.scm().create_patch = Mock(return_value="Patch1\nMockPatch\n")
+        mock_tool.checkout().modified_changelogs = Mock(return_value=[])
+        self.assert_execute_outputs(Land(), [50000], options=self._default_options(), expected_stderr=expected_stderr, tool=mock_tool)
+        # Make sure we're not calling expensive calls too often.
+        self.assertEqual(mock_tool.scm().create_patch.call_count, 0)
+        self.assertEqual(mock_tool.checkout().modified_changelogs.call_count, 1)
+
+    def test_land_cowboy(self):
+        expected_stderr = """MOCK run_and_throw_if_fail: ['mock-prepare-ChangeLog', '--email=MOCK email', '--merge-base=None', 'MockFile1'], cwd=/mock-checkout
+MOCK run_and_throw_if_fail: ['mock-check-webkit-style', '--git-commit', 'MOCK git commit', '--diff-files', 'MockFile1', '--filter', '-changelog'], cwd=/mock-checkout
+MOCK run_command: ['ruby', '-I', '/mock-checkout/Websites/bugs.webkit.org/PrettyPatch', '/mock-checkout/Websites/bugs.webkit.org/PrettyPatch/prettify.rb'], cwd=None, input=Patch1
+MOCK: user.open_url: file://...
+Was that diff correct?
+Building WebKit
+MOCK run_and_throw_if_fail: ['mock-build-webkit'], cwd=/mock-checkout, env={'LC_ALL': 'C', 'MOCK_ENVIRON_COPY': '1'}
+Running Python unit tests
+MOCK run_and_throw_if_fail: ['mock-test-webkitpy'], cwd=/mock-checkout
+Running Perl unit tests
+MOCK run_and_throw_if_fail: ['mock-test-webkitperl'], cwd=/mock-checkout
+Running JavaScriptCore tests
+MOCK run_and_throw_if_fail: ['mock-run-javacriptcore-tests'], cwd=/mock-checkout
+Running WebKit unit tests
+MOCK run_and_throw_if_fail: ['mock-run-webkit-unit-tests'], cwd=/mock-checkout
+Running run-webkit-tests
+MOCK run_and_throw_if_fail: ['mock-run-webkit-tests', '--quiet'], cwd=/mock-checkout
+Committed r49824: <http://trac.webkit.org/changeset/49824>
+Committed r49824: <http://trac.webkit.org/changeset/49824>
+No bug id provided.
+"""
+        mock_tool = MockTool(log_executive=True)
+        self.assert_execute_outputs(LandCowboy(), [50000], options=self._default_options(), expected_stderr=expected_stderr, tool=mock_tool)
+
+    def test_land_red_builders(self):
+        expected_stderr = 'Building WebKit\nRunning Python unit tests\nRunning Perl unit tests\nRunning JavaScriptCore tests\nRunning WebKit unit tests\nRunning run-webkit-tests\nCommitted r49824: <http://trac.webkit.org/changeset/49824>\nUpdating bug 50000\n'
+        mock_tool = MockTool()
+        mock_tool.buildbot.light_tree_on_fire()
+        self.assert_execute_outputs(Land(), [50000], options=self._default_options(), expected_stderr=expected_stderr, tool=mock_tool)
+
+    def test_check_style(self):
+        expected_stderr = """Processing 1 patch from 1 bug.
+Updating working directory
+MOCK run_and_throw_if_fail: ['mock-update-webkit'], cwd=/mock-checkout
+Processing patch 10000 from bug 50000.
+MOCK run_and_throw_if_fail: ['mock-check-webkit-style', '--git-commit', 'MOCK git commit', '--diff-files', 'MockFile1'], cwd=/mock-checkout
+"""
+        self.assert_execute_outputs(CheckStyle(), [10000], options=self._default_options(), expected_stderr=expected_stderr, tool=MockTool(log_executive=True))
+
+    def test_build_attachment(self):
+        expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 10000 from bug 50000.\nBuilding WebKit\n"
+        self.assert_execute_outputs(BuildAttachment(), [10000], options=self._default_options(), expected_stderr=expected_stderr)
+
+    def test_land_attachment(self):
+        # FIXME: This expected result is imperfect, notice how it's seeing the same patch as still there after it thought it would have cleared the flags.
+        expected_stderr = """Processing 1 patch from 1 bug.
+Updating working directory
+Processing patch 10000 from bug 50000.
+Building WebKit
+Running Python unit tests
+Running Perl unit tests
+Running JavaScriptCore tests
+Running WebKit unit tests
+Running run-webkit-tests
+Committed r49824: <http://trac.webkit.org/changeset/49824>
+Not closing bug 50000 as attachment 10000 has review=+.  Assuming there are more patches to land from this bug.
+"""
+        self.assert_execute_outputs(LandAttachment(), [10000], options=self._default_options(), expected_stderr=expected_stderr)
+
+    def test_land_from_bug(self):
+        # FIXME: This expected result is imperfect, notice how it's seeing the same patch as still there after it thought it would have cleared the flags.
+        expected_stderr = """2 reviewed patches found on bug 50000.
+Processing 2 patches from 1 bug.
+Updating working directory
+Processing patch 10000 from bug 50000.
+Building WebKit
+Running Python unit tests
+Running Perl unit tests
+Running JavaScriptCore tests
+Running WebKit unit tests
+Running run-webkit-tests
+Committed r49824: <http://trac.webkit.org/changeset/49824>
+Not closing bug 50000 as attachment 10000 has review=+.  Assuming there are more patches to land from this bug.
+Updating working directory
+Processing patch 10001 from bug 50000.
+Building WebKit
+Running Python unit tests
+Running Perl unit tests
+Running JavaScriptCore tests
+Running WebKit unit tests
+Running run-webkit-tests
+Committed r49824: <http://trac.webkit.org/changeset/49824>
+Not closing bug 50000 as attachment 10000 has review=+.  Assuming there are more patches to land from this bug.
+"""
+        self.assert_execute_outputs(LandFromBug(), [50000], options=self._default_options(), expected_stderr=expected_stderr)
+
+    def test_land_from_url(self):
+        # FIXME: This expected result is imperfect, notice how it's seeing the same patch as still there after it thought it would have cleared the flags.
+        expected_stderr = """2 patches found on bug 50000.
+Processing 2 patches from 1 bug.
+Updating working directory
+Processing patch 10000 from bug 50000.
+Building WebKit
+Running Python unit tests
+Running Perl unit tests
+Running JavaScriptCore tests
+Running WebKit unit tests
+Running run-webkit-tests
+Committed r49824: <http://trac.webkit.org/changeset/49824>
+Not closing bug 50000 as attachment 10000 has review=+.  Assuming there are more patches to land from this bug.
+Updating working directory
+Processing patch 10001 from bug 50000.
+Building WebKit
+Running Python unit tests
+Running Perl unit tests
+Running JavaScriptCore tests
+Running WebKit unit tests
+Running run-webkit-tests
+Committed r49824: <http://trac.webkit.org/changeset/49824>
+Not closing bug 50000 as attachment 10000 has review=+.  Assuming there are more patches to land from this bug.
+"""
+        self.assert_execute_outputs(LandFromURL(), ["https://bugs.webkit.org/show_bug.cgi?id=50000"], options=self._default_options(), expected_stderr=expected_stderr)
+
+    def test_prepare_rollout(self):
+        expected_stderr = "Preparing rollout for bug 50000.\nUpdating working directory\n"
+        self.assert_execute_outputs(PrepareRollout(), [852, "Reason"], options=self._default_options(), expected_stderr=expected_stderr)
+
+    def test_create_rollout(self):
+        expected_stderr = """Preparing rollout for bug 50000.
+Updating working directory
+MOCK create_bug
+bug_title: REGRESSION(r852): Reason
+bug_description: http://trac.webkit.org/changeset/852 broke the build:
+Reason
+component: MOCK component
+cc: MOCK cc
+blocked: 50000
+MOCK add_patch_to_bug: bug_id=60001, description=ROLLOUT of r852, mark_for_review=False, mark_for_commit_queue=True, mark_for_landing=False
+-- Begin comment --
+Any committer can land this patch automatically by marking it commit-queue+.  The commit-queue will build and test the patch before landing to ensure that the rollout will be successful.  This process takes approximately 15 minutes.
+
+If you would like to land the rollout faster, you can use the following command:
+
+  webkit-patch land-attachment ATTACHMENT_ID
+
+where ATTACHMENT_ID is the ID of this attachment.
+-- End comment --
+"""
+        self.assert_execute_outputs(CreateRollout(), [852, "Reason"], options=self._default_options(), expected_stderr=expected_stderr)
+        self.assert_execute_outputs(CreateRollout(), ["855 852 854", "Reason"], options=self._default_options(), expected_stderr=expected_stderr)
+
+    def test_create_rollout_resolved(self):
+        expected_stderr = """Preparing rollout for bug 50004.
+Updating working directory
+MOCK create_bug
+bug_title: REGRESSION(r3001): Reason
+bug_description: http://trac.webkit.org/changeset/3001 broke the build:
+Reason
+component: MOCK component
+cc: MOCK cc
+blocked: 50004
+MOCK reopen_bug 50004 with comment 'Re-opened since this is blocked by bug 60001'
+MOCK add_patch_to_bug: bug_id=60001, description=ROLLOUT of r3001, mark_for_review=False, mark_for_commit_queue=True, mark_for_landing=False
+-- Begin comment --
+Any committer can land this patch automatically by marking it commit-queue+.  The commit-queue will build and test the patch before landing to ensure that the rollout will be successful.  This process takes approximately 15 minutes.
+
+If you would like to land the rollout faster, you can use the following command:
+
+  webkit-patch land-attachment ATTACHMENT_ID
+
+where ATTACHMENT_ID is the ID of this attachment.
+-- End comment --
+"""
+        self.assert_execute_outputs(CreateRollout(), [3001, "Reason"], options=self._default_options(), expected_stderr=expected_stderr)
+
+    def test_rollout(self):
+        expected_stderr = """Preparing rollout for bug 50000.
+Updating working directory
+MOCK: user.open_url: file://...
+Was that diff correct?
+Building WebKit
+Committed r49824: <http://trac.webkit.org/changeset/49824>
+MOCK reopen_bug 50000 with comment 'Reverted r852 for reason:
+
+Reason
+
+Committed r49824: <http://trac.webkit.org/changeset/49824>'
+"""
+        self.assert_execute_outputs(Rollout(), [852, "Reason"], options=self._default_options(), expected_stderr=expected_stderr)
+
diff --git a/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem.py b/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem.py
new file mode 100644
index 0000000..b7d5df3
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem.py
@@ -0,0 +1,212 @@
+# Copyright (c) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from optparse import make_option
+
+from webkitpy.common.config.committers import CommitterList
+from webkitpy.common.config.ports import DeprecatedPort
+from webkitpy.common.system.deprecated_logging import error, log
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.tool.bot.earlywarningsystemtask import EarlyWarningSystemTask, EarlyWarningSystemTaskDelegate
+from webkitpy.tool.bot.expectedfailures import ExpectedFailures
+from webkitpy.tool.bot.layouttestresultsreader import LayoutTestResultsReader
+from webkitpy.tool.bot.patchanalysistask import UnableToApplyPatch
+from webkitpy.tool.bot.queueengine import QueueEngine
+from webkitpy.tool.commands.queues import AbstractReviewQueue
+
+
+class AbstractEarlyWarningSystem(AbstractReviewQueue, EarlyWarningSystemTaskDelegate):
+    _build_style = "release"
+    # FIXME: Switch _default_run_tests from opt-in to opt-out once more bots are ready to run tests.
+    _default_run_tests = False
+
+    def __init__(self):
+        options = [make_option("--run-tests", action="store_true", dest="run_tests", default=self._default_run_tests, help="Run the Layout tests for each patch")]
+        AbstractReviewQueue.__init__(self, options=options)
+        self.port = DeprecatedPort.port(self.port_name)
+
+    def begin_work_queue(self):
+        # FIXME: This violates abstraction
+        self._tool._deprecated_port = self.port
+        AbstractReviewQueue.begin_work_queue(self)
+        self._expected_failures = ExpectedFailures()
+        self._layout_test_results_reader = LayoutTestResultsReader(self._tool, self._log_directory())
+
+    def _failing_tests_message(self, task, patch):
+        results = task.results_from_patch_test_run(patch)
+        unexpected_failures = self._expected_failures.unexpected_failures_observed(results)
+        if not unexpected_failures:
+            return None
+        return "New failing tests:\n%s" % "\n".join(unexpected_failures)
+
+    def _post_reject_message_on_bug(self, tool, patch, status_id, extra_message_text=None):
+        results_link = tool.status_server.results_url_for_status(status_id)
+        message = "Attachment %s did not pass %s (%s):\nOutput: %s" % (patch.id(), self.name, self.port_name, results_link)
+        # FIXME: We might want to add some text about rejecting from the commit-queue in
+        # the case where patch.commit_queue() isn't already set to '-'.
+        if self.watchers:
+            tool.bugs.add_cc_to_bug(patch.bug_id(), self.watchers)
+        tool.bugs.set_flag_on_attachment(patch.id(), "commit-queue", "-", message, extra_message_text)
+
+    def review_patch(self, patch):
+        task = EarlyWarningSystemTask(self, patch, self._options.run_tests)
+        if not task.validate():
+            self._did_error(patch, "%s did not process patch." % self.name)
+            return False
+        try:
+            return task.run()
+        except UnableToApplyPatch, e:
+            self._did_error(patch, "%s unable to apply patch." % self.name)
+            return False
+        except ScriptError, e:
+            self._post_reject_message_on_bug(self._tool, patch, task.failure_status_id, self._failing_tests_message(task, patch))
+            results_archive = task.results_archive_from_patch_test_run(patch)
+            if results_archive:
+                self._upload_results_archive_for_patch(patch, results_archive)
+            self._did_fail(patch)
+            # FIXME: We're supposed to be able to raise e again here and have
+            # one of our base classes mark the patch as fail, but there seems
+            # to be an issue with the exit_code.
+            return False
+
+    # EarlyWarningSystemDelegate methods
+
+    def parent_command(self):
+        return self.name
+
+    def run_command(self, command):
+        self.run_webkit_patch(command + [self.port.flag()])
+
+    def command_passed(self, message, patch):
+        pass
+
+    def command_failed(self, message, script_error, patch):
+        failure_log = self._log_from_script_error_for_upload(script_error)
+        return self._update_status(message, patch=patch, results_file=failure_log)
+
+    def expected_failures(self):
+        return self._expected_failures
+
+    def test_results(self):
+        return self._layout_test_results_reader.results()
+
+    def archive_last_test_results(self, patch):
+        return self._layout_test_results_reader.archive(patch)
+
+    def build_style(self):
+        return self._build_style
+
+    def refetch_patch(self, patch):
+        return self._tool.bugs.fetch_attachment(patch.id())
+
+    def report_flaky_tests(self, patch, flaky_test_results, results_archive):
+        pass
+
+    # StepSequenceErrorHandler methods
+
+    @classmethod
+    def handle_script_error(cls, tool, state, script_error):
+        # FIXME: Why does this not exit(1) like the superclass does?
+        log(script_error.message_with_output())
+
+
+class GtkEWS(AbstractEarlyWarningSystem):
+    name = "gtk-ews"
+    port_name = "gtk"
+    watchers = AbstractEarlyWarningSystem.watchers + [
+        "gns@gnome.org",
+        "xan.lopez@gmail.com",
+    ]
+
+
+class EflEWS(AbstractEarlyWarningSystem):
+    name = "efl-ews"
+    port_name = "efl"
+    watchers = AbstractEarlyWarningSystem.watchers + [
+        "leandro@profusion.mobi",
+        "antognolli@profusion.mobi",
+        "lucas.demarchi@profusion.mobi",
+        "gyuyoung.kim@samsung.com",
+    ]
+
+
+class QtEWS(AbstractEarlyWarningSystem):
+    name = "qt-ews"
+    port_name = "qt"
+    watchers = AbstractEarlyWarningSystem.watchers + [
+        "webkit-ews@sed.inf.u-szeged.hu",
+    ]
+
+
+class QtWK2EWS(AbstractEarlyWarningSystem):
+    name = "qt-wk2-ews"
+    port_name = "qt"
+    watchers = AbstractEarlyWarningSystem.watchers + [
+        "webkit-ews@sed.inf.u-szeged.hu",
+    ]
+
+
+class WinEWS(AbstractEarlyWarningSystem):
+    name = "win-ews"
+    port_name = "win"
+    # Use debug, the Apple Win port fails to link Release on 32-bit Windows.
+    # https://bugs.webkit.org/show_bug.cgi?id=39197
+    _build_style = "debug"
+
+
+class AbstractChromiumEWS(AbstractEarlyWarningSystem):
+    port_name = "chromium"
+    watchers = AbstractEarlyWarningSystem.watchers + [
+        "dglazkov@chromium.org",
+    ]
+
+
+class ChromiumLinuxEWS(AbstractChromiumEWS):
+    # FIXME: We should rename this command to cr-linux-ews, but that requires
+    #        a database migration. :(
+    name = "chromium-ews"
+    port_name = "chromium-xvfb"
+    _default_run_tests = True
+
+
+class ChromiumWindowsEWS(AbstractChromiumEWS):
+    name = "cr-win-ews"
+
+
+class ChromiumAndroidEWS(AbstractChromiumEWS):
+    name = "cr-android-ews"
+    port_name = "chromium-android"
+    watchers = AbstractChromiumEWS.watchers + [
+        "peter+ews@chromium.org",
+    ]
+
+
+class MacEWS(AbstractEarlyWarningSystem):
+    name = "mac-ews"
+    port_name = "mac"
+    _default_run_tests = True
diff --git a/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py b/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py
new file mode 100644
index 0000000..7feff0d
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py
@@ -0,0 +1,92 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.tool.bot.queueengine import QueueEngine
+from webkitpy.tool.commands.earlywarningsystem import *
+from webkitpy.tool.commands.queuestest import QueuesTest
+from webkitpy.tool.mocktool import MockTool, MockOptions
+
+
+class AbstractEarlyWarningSystemTest(QueuesTest):
+    def test_failing_tests_message(self):
+        # Needed to define port_name, used in AbstractEarlyWarningSystem.__init__
+        class TestEWS(AbstractEarlyWarningSystem):
+            port_name = "win"  # Needs to be a port which port/factory understands.
+
+        ews = TestEWS()
+        ews.bind_to_tool(MockTool())
+        ews._options = MockOptions(port=None, confirm=False)
+        OutputCapture().assert_outputs(self, ews.begin_work_queue, expected_stderr=self._default_begin_work_queue_stderr(ews.name))
+        ews._expected_failures.unexpected_failures_observed = lambda results: set(["foo.html", "bar.html"])
+        task = Mock()
+        patch = ews._tool.bugs.fetch_attachment(10000)
+        self.assertEqual(ews._failing_tests_message(task, patch), "New failing tests:\nbar.html\nfoo.html")
+
+
+class EarlyWarningSytemTest(QueuesTest):
+    def _default_expected_stderr(self, ews):
+        string_replacemnts = {
+            "name": ews.name,
+            "port": ews.port_name,
+        }
+        expected_stderr = {
+            "begin_work_queue": self._default_begin_work_queue_stderr(ews.name),
+            "handle_unexpected_error": "Mock error message\n",
+            "next_work_item": "",
+            "process_work_item": "MOCK: update_status: %(name)s Pass\nMOCK: release_work_item: %(name)s 10000\n" % string_replacemnts,
+            "handle_script_error": "ScriptError error message\n\nMOCK output\n",
+        }
+        return expected_stderr
+
+    def _test_builder_ews(self, ews):
+        ews.bind_to_tool(MockTool())
+        options = Mock()
+        options.port = None
+        options.run_tests = ews._default_run_tests
+        self.assert_queue_outputs(ews, expected_stderr=self._default_expected_stderr(ews), options=options)
+
+    def _test_testing_ews(self, ews):
+        ews.test_results = lambda: None
+        ews.bind_to_tool(MockTool())
+        expected_stderr = self._default_expected_stderr(ews)
+        expected_stderr["handle_script_error"] = "ScriptError error message\n\nMOCK output\n"
+        self.assert_queue_outputs(ews, expected_stderr=expected_stderr)
+
+    def test_builder_ewses(self):
+        self._test_builder_ews(MacEWS())
+        self._test_builder_ews(ChromiumWindowsEWS())
+        self._test_builder_ews(ChromiumAndroidEWS())
+        self._test_builder_ews(QtEWS())
+        self._test_builder_ews(QtWK2EWS())
+        self._test_builder_ews(GtkEWS())
+        self._test_builder_ews(EflEWS())
+
+    def test_testing_ewses(self):
+        self._test_testing_ews(ChromiumLinuxEWS())
diff --git a/Tools/Scripts/webkitpy/tool/commands/expectations.py b/Tools/Scripts/webkitpy/tool/commands/expectations.py
new file mode 100644
index 0000000..0e1050b
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/expectations.py
@@ -0,0 +1,44 @@
+# Copyright (c) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.layout_tests.models.test_configuration import TestConfigurationConverter
+from webkitpy.layout_tests.models.test_expectations import TestExpectationParser
+from webkitpy.layout_tests.models.test_expectations import TestExpectations
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+
+
+class OptimizeExpectations(AbstractDeclarativeCommand):
+    name = "optimize-expectations"
+    help_text = "Fixes simple style issues in test_expectations file.  (Currently works only for chromium port.)"
+
+    def execute(self, options, args, tool):
+        port = tool.port_factory.get("chromium-win-win7")  # FIXME: This should be selectable.
+        parser = TestExpectationParser(port, [], allow_rebaseline_modifier=False)
+        expectation_lines = parser.parse(port.test_expectations())
+        converter = TestConfigurationConverter(port.all_test_configurations(), port.configuration_specifier_macros())
+        tool.filesystem.write_text_file(port.path_to_test_expectations_file(), TestExpectations.list_to_string(expectation_lines, converter))
diff --git a/Tools/Scripts/webkitpy/tool/commands/findusers.py b/Tools/Scripts/webkitpy/tool/commands/findusers.py
new file mode 100644
index 0000000..4363c8c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/findusers.py
@@ -0,0 +1,44 @@
+# Copyright (c) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+
+
+class FindUsers(AbstractDeclarativeCommand):
+    name = "find-users"
+    help_text = "Find users matching substring"
+
+    def execute(self, options, args, tool):
+        search_string = args[0]
+        login_userid_pairs = tool.bugs.queries.fetch_login_userid_pairs_matching_substring(search_string)
+        for (login, user_id) in login_userid_pairs:
+            user = tool.bugs.fetch_user(user_id)
+            groups_string = ", ".join(user['groups']) if user['groups'] else "none"
+            print "%s <%s> (%s) (%s)" % (user['name'], user['login'], user_id, groups_string)
+        else:
+            print "No users found matching '%s'" % search_string
diff --git a/Tools/Scripts/webkitpy/tool/commands/gardenomatic.py b/Tools/Scripts/webkitpy/tool/commands/gardenomatic.py
new file mode 100644
index 0000000..6cb1519
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/gardenomatic.py
@@ -0,0 +1,59 @@
+# Copyright (C) 2011 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:
+#
+# 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.layout_tests.port import builders
+from webkitpy.tool.commands.rebaseline import AbstractRebaseliningCommand
+from webkitpy.tool.servers.gardeningserver import GardeningHTTPServer
+
+
+class GardenOMatic(AbstractRebaseliningCommand):
+    name = "garden-o-matic"
+    help_text = "Command for gardening the WebKit tree."
+
+    def __init__(self):
+        return super(AbstractRebaseliningCommand, self).__init__(options=(self.platform_options + [
+            self.move_overwritten_baselines_option,
+            self.results_directory_option,
+            self.no_optimize_option,
+            ]))
+
+    def execute(self, options, args, tool):
+        print "This command runs a local HTTP server that changes your working copy"
+        print "based on the actions you take in the web-based UI."
+
+        args = {}
+        if options.platform:
+            # FIXME: This assumes that the port implementation (chromium-, gtk-, etc.) is the first part of options.platform.
+            args['platform'] = options.platform.split('-')[0]
+            builder = builders.builder_name_for_port_name(options.platform)
+            if builder:
+                args['builder'] = builder
+        if options.results_directory:
+            args['useLocalResults'] = "true"
+
+        httpd = GardeningHTTPServer(httpd_port=8127, config={'tool': tool, 'options': options})
+        self._tool.user.open_url(httpd.url(args))
+
+        print "Local HTTP server started."
+        httpd.serve_forever()
diff --git a/Tools/Scripts/webkitpy/tool/commands/openbugs.py b/Tools/Scripts/webkitpy/tool/commands/openbugs.py
new file mode 100644
index 0000000..1b51c9f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/openbugs.py
@@ -0,0 +1,63 @@
+# 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 re
+import sys
+
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+from webkitpy.common.system.deprecated_logging import log
+
+
+class OpenBugs(AbstractDeclarativeCommand):
+    name = "open-bugs"
+    help_text = "Finds all bug numbers passed in arguments (or stdin if no args provided) and opens them in a web browser"
+
+    bug_number_regexp = re.compile(r"\b\d{4,6}\b")
+
+    def _open_bugs(self, bug_ids):
+        for bug_id in bug_ids:
+            bug_url = self._tool.bugs.bug_url_for_bug_id(bug_id)
+            self._tool.user.open_url(bug_url)
+
+    # _find_bugs_in_string mostly exists for easy unit testing.
+    def _find_bugs_in_string(self, string):
+        return self.bug_number_regexp.findall(string)
+
+    def _find_bugs_in_iterable(self, iterable):
+        return sum([self._find_bugs_in_string(string) for string in iterable], [])
+
+    def execute(self, options, args, tool):
+        if args:
+            bug_ids = self._find_bugs_in_iterable(args)
+        else:
+            # This won't open bugs until stdin is closed but could be made to easily.  That would just make unit testing slightly harder.
+            bug_ids = self._find_bugs_in_iterable(sys.stdin)
+
+        log("%s bugs found in input." % len(bug_ids))
+
+        self._open_bugs(bug_ids)
diff --git a/Tools/Scripts/webkitpy/tool/commands/openbugs_unittest.py b/Tools/Scripts/webkitpy/tool/commands/openbugs_unittest.py
new file mode 100644
index 0000000..40a6e1b
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/openbugs_unittest.py
@@ -0,0 +1,50 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.commands.openbugs import OpenBugs
+
+class OpenBugsTest(CommandsTest):
+
+    find_bugs_in_string_expectations = [
+        ["123", []],
+        ["1234", ["1234"]],
+        ["12345", ["12345"]],
+        ["123456", ["123456"]],
+        ["1234567", []],
+        [" 123456 234567", ["123456", "234567"]],
+    ]
+
+    def test_find_bugs_in_string(self):
+        openbugs = OpenBugs()
+        for expectation in self.find_bugs_in_string_expectations:
+            self.assertEquals(openbugs._find_bugs_in_string(expectation[0]), expectation[1])
+
+    def test_args_parsing(self):
+        expected_stderr = "2 bugs found in input.\nMOCK: user.open_url: http://example.com/12345\nMOCK: user.open_url: http://example.com/23456\n"
+        self.assert_execute_outputs(OpenBugs(), ["12345\n23456"], expected_stderr=expected_stderr)
diff --git a/Tools/Scripts/webkitpy/tool/commands/perfalizer.py b/Tools/Scripts/webkitpy/tool/commands/perfalizer.py
new file mode 100644
index 0000000..ae9f63a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/perfalizer.py
@@ -0,0 +1,215 @@
+# Copyright (c) 2012 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.tool.bot.expectedfailures import ExpectedFailures
+from webkitpy.tool.bot.irc_command import IRCCommand
+from webkitpy.tool.bot.irc_command import Help
+from webkitpy.tool.bot.irc_command import Hi
+from webkitpy.tool.bot.irc_command import Restart
+from webkitpy.tool.bot.ircbot import IRCBot
+from webkitpy.tool.bot.patchanalysistask import PatchAnalysisTask, PatchAnalysisTaskDelegate, UnableToApplyPatch
+from webkitpy.tool.bot.sheriff import Sheriff
+from webkitpy.tool.commands.queues import AbstractQueue
+from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler
+
+
+class PerfalizerTask(PatchAnalysisTask):
+    def __init__(self, tool, patch, logger):
+        PatchAnalysisTask.__init__(self, self, patch)
+        self._port = tool.port_factory.get()
+        self._tool = tool
+        self._logger = logger
+
+    def _copy_build_product_without_patch(self):
+        filesystem = self._tool.filesystem
+        configuration = filesystem.basename(self._port._build_path())
+        self._build_directory = filesystem.dirname(self._port._build_path())
+        self._build_directory_without_patch = self._build_directory + 'WithoutPatch'
+
+        try:
+            filesystem.rmtree(self._build_directory_without_patch)
+            filesystem.copytree(filesystem.join(self._build_directory, configuration),
+                filesystem.join(self._build_directory_without_patch, configuration))
+            return True
+        except:
+            return False
+
+    def run(self):
+        if not self._patch.committer() and not self._patch.attacher().can_commit:
+            self._logger('The patch %d is not authorized by a commmitter' % self._patch.id())
+            return False
+
+        self._logger('Preparing to run performance tests for the attachment %d...' % self._patch.id())
+        if not self._clean() or not self._update():
+            return False
+
+        head_revision = self._tool.scm().head_svn_revision()
+
+        self._logger('Building WebKit at r%s without the patch' % head_revision)
+        if not self._build_without_patch():
+            return False
+
+        if not self._port.check_build(needs_http=False):
+            self._logger('Failed to build DumpRenderTree.')
+            return False
+
+        if not self._copy_build_product_without_patch():
+            self._logger('Failed to copy the build product from %s to %s' % (self._build_directory, self._build_directory_without_patch))
+            return False
+
+        self._logger('Building WebKit at r%s with the patch' % head_revision)
+        if not self._apply() or not self._build():
+            return False
+
+        if not self._port.check_build(needs_http=False):
+            self._logger('Failed to build DumpRenderTree.')
+            return False
+
+        filesystem = self._tool.filesystem
+        if filesystem.exists(self._json_path()):
+            filesystem.remove(self._json_path())
+
+        self._logger("Running performance tests...")
+        if self._run_perf_test(self._build_directory_without_patch, 'without %d' % self._patch.id()) < 0:
+            self._logger('Failed to run performance tests without the patch.')
+            return False
+
+        if self._run_perf_test(self._build_directory, 'with %d' % self._patch.id()) < 0:
+            self._logger('Failed to run performance tests with the patch.')
+            return False
+
+        if not filesystem.exists(self._results_page_path()):
+            self._logger('Failed to generate the results page.')
+            return False
+
+        results_page = filesystem.read_text_file(self._results_page_path())
+        self._tool.bugs.add_attachment_to_bug(self._patch.bug_id(), results_page,
+            description="Performance tests results for %d" % self._patch.id(), mimetype='text/html')
+
+        self._logger("Uploaded the results on the bug %d" % self._patch.bug_id())
+        return True
+
+    def parent_command(self):
+        return "perfalizer"
+
+    def run_webkit_patch(self, args):
+        webkit_patch_args = [self._tool.path()]
+        webkit_patch_args.extend(args)
+        return self._tool.executive.run_and_throw_if_fail(webkit_patch_args, cwd=self._tool.scm().checkout_root)
+
+    def _json_path(self):
+        return self._tool.filesystem.join(self._build_directory, 'PerformanceTestResults.json')
+
+    def _results_page_path(self):
+        return self._tool.filesystem.join(self._build_directory, 'PerformanceTestResults.html')
+
+    def _run_perf_test(self, build_path, description):
+        filesystem = self._tool.filesystem
+        script_path = filesystem.join(filesystem.dirname(self._tool.path()), 'run-perf-tests')
+        perf_test_runner_args = [script_path, '--no-build', '--no-show-results', '--build-directory', build_path,
+            '--output-json-path', self._json_path(), '--description', description]
+        return self._tool.executive.run_and_throw_if_fail(perf_test_runner_args, cwd=self._tool.scm().checkout_root)
+
+    def run_command(self, command):
+        self.run_webkit_patch(command)
+
+    def command_passed(self, message, patch):
+        pass
+
+    def command_failed(self, message, script_error, patch):
+        self._logger(message)
+
+    def refetch_patch(self, patch):
+        return self._tool.bugs.fetch_attachment(patch.id())
+
+    def expected_failures(self):
+        return ExpectedFailures()
+
+    def build_style(self):
+        return "release"
+
+
+class PerfTest(IRCCommand):
+    def execute(self, nick, args, tool, sheriff):
+        if not args:
+            tool.irc().post(nick + ": Please specify an attachment/patch id")
+            return
+
+        patch_id = args[0]
+        patch = tool.bugs.fetch_attachment(patch_id)
+        if not patch:
+            tool.irc().post(nick + ": Could not fetch the patch")
+            return
+
+        task = PerfalizerTask(tool, patch, lambda message: tool.irc().post('%s: %s' % (nick, message)))
+        task.run()
+
+
+class Perfalizer(AbstractQueue, StepSequenceErrorHandler):
+    name = "perfalizer"
+    watchers = AbstractQueue.watchers + ["rniwa@webkit.org"]
+
+    _commands = {
+        "help": Help,
+        "hi": Hi,
+        "restart": Restart,
+        "test": PerfTest,
+    }
+
+    # AbstractQueue methods
+
+    def begin_work_queue(self):
+        AbstractQueue.begin_work_queue(self)
+        self._sheriff = Sheriff(self._tool, self)
+        self._irc_bot = IRCBot("perfalizer", self._tool, self._sheriff, self._commands)
+        self._tool.ensure_irc_connected(self._irc_bot.irc_delegate())
+
+    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()
+        return
+
+    def process_work_item(self, failure_map):
+        return True
+
+    def handle_unexpected_error(self, failure_map, message):
+        log(message)
+
+    # StepSequenceErrorHandler methods
+
+    @classmethod
+    def handle_script_error(cls, tool, state, script_error):
+        # Ideally we would post some information to IRC about what went wrong
+        # here, but we don't have the IRC password in the child process.
+        pass
diff --git a/Tools/Scripts/webkitpy/tool/commands/perfalizer_unittest.py b/Tools/Scripts/webkitpy/tool/commands/perfalizer_unittest.py
new file mode 100644
index 0000000..feb7b05
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/perfalizer_unittest.py
@@ -0,0 +1,112 @@
+# Copyright (C) 2012 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 Builder
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.layout_tests.port.test import TestPort
+from webkitpy.tool.commands.perfalizer import PerfalizerTask
+from webkitpy.tool.mocktool import MockTool
+
+
+class PerfalizerTaskTest(unittest.TestCase):
+    def _create_and_run_perfalizer(self, commands_to_fail=[]):
+        tool = MockTool()
+        patch = tool.bugs.fetch_attachment(10000)
+
+        logs = []
+
+        def logger(message):
+            logs.append(message)
+
+        def run_webkit_patch(args):
+            if args[0] in commands_to_fail:
+                raise ScriptError
+
+        def run_perf_test(build_path, description):
+            self.assertTrue(description == 'without 10000' or description == 'with 10000')
+            if 'run-perf-tests' in commands_to_fail:
+                return -1
+            if 'results-page' not in commands_to_fail:
+                tool.filesystem.write_text_file(tool.filesystem.join(build_path, 'PerformanceTestResults.html'), 'results page')
+            return 0
+
+        perfalizer = PerfalizerTask(tool, patch, logger)
+        perfalizer._port = TestPort(tool)
+        perfalizer.run_webkit_patch = run_webkit_patch
+        perfalizer._run_perf_test = run_perf_test
+
+        capture = OutputCapture()
+        capture.capture_output()
+
+        if commands_to_fail:
+            self.assertFalse(perfalizer.run())
+        else:
+            self.assertTrue(perfalizer.run())
+
+        capture.restore_output()
+
+        return logs
+
+    def test_run(self):
+        self.assertEqual(self._create_and_run_perfalizer(), [
+            'Preparing to run performance tests for the attachment 10000...',
+            'Building WebKit at r1234 without the patch',
+            'Building WebKit at r1234 with the patch',
+            'Running performance tests...',
+            'Uploaded the results on the bug 50000'])
+
+    def test_run_with_clean_fails(self):
+        self.assertEqual(self._create_and_run_perfalizer(['clean']), [
+            'Preparing to run performance tests for the attachment 10000...',
+            'Unable to clean working directory'])
+
+    def test_run_with_update_fails(self):
+        logs = self._create_and_run_perfalizer(['update'])
+        self.assertEqual(len(logs), 2)
+        self.assertEqual(logs[-1], 'Unable to update working directory')
+
+    def test_run_with_build_fails(self):
+        logs = self._create_and_run_perfalizer(['build'])
+        self.assertEqual(len(logs), 3)
+
+    def test_run_with_build_fails(self):
+        logs = self._create_and_run_perfalizer(['apply-attachment'])
+        self.assertEqual(len(logs), 4)
+
+    def test_run_with_perf_test_fails(self):
+        logs = self._create_and_run_perfalizer(['run-perf-tests'])
+        self.assertEqual(len(logs), 5)
+        self.assertEqual(logs[-1], 'Failed to run performance tests without the patch.')
+
+    def test_run_without_results_page(self):
+        logs = self._create_and_run_perfalizer(['results-page'])
+        self.assertEqual(len(logs), 5)
+        self.assertEqual(logs[-1], 'Failed to generate the results page.')
diff --git a/Tools/Scripts/webkitpy/tool/commands/prettydiff.py b/Tools/Scripts/webkitpy/tool/commands/prettydiff.py
new file mode 100644
index 0000000..66a06a6
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/prettydiff.py
@@ -0,0 +1,39 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand
+from webkitpy.tool import steps
+
+
+class PrettyDiff(AbstractSequencedCommand):
+    name = "pretty-diff"
+    help_text = "Shows the pretty diff in the default browser"
+    show_in_main_help = True
+    steps = [
+        steps.ConfirmDiff,
+    ]
diff --git a/Tools/Scripts/webkitpy/tool/commands/queries.py b/Tools/Scripts/webkitpy/tool/commands/queries.py
new file mode 100644
index 0000000..b7e4a85
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/queries.py
@@ -0,0 +1,568 @@
+# Copyright (c) 2009 Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+# Copyright (c) 2012 Intel Corporation. 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 fnmatch
+import re
+
+from datetime import datetime
+from optparse import make_option
+
+from webkitpy.tool import steps
+
+from webkitpy.common.checkout.commitinfo import CommitInfo
+from webkitpy.common.config.committers import CommitterList
+import webkitpy.common.config.urls as config_urls
+from webkitpy.common.net.buildbot import BuildBot
+from webkitpy.common.net.regressionwindow import RegressionWindow
+from webkitpy.common.system.crashlogs import CrashLogs
+from webkitpy.common.system.user import User
+from webkitpy.tool.grammar import pluralize
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.layout_tests.models.test_expectations import TestExpectations
+from webkitpy.layout_tests.port import platform_options, configuration_options
+
+
+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"
+
+    def execute(self, options, args, tool):
+        # FIXME: This command is poorly named.  It's fetching the commit-queue list here.  The name implies it's fetching pending-commit (all r+'d patches).
+        bug_ids = tool.bugs.queries.fetch_bug_ids_from_commit_queue()
+        for bug_id in bug_ids:
+            print "%s" % bug_id
+
+
+class PatchesInCommitQueue(AbstractDeclarativeCommand):
+    name = "patches-in-commit-queue"
+    help_text = "List patches in the commit-queue"
+
+    def execute(self, options, args, tool):
+        patches = tool.bugs.queries.fetch_patches_from_commit_queue()
+        log("Patches in commit queue:")
+        for patch in patches:
+            print patch.url()
+
+
+class PatchesToCommitQueue(AbstractDeclarativeCommand):
+    name = "patches-to-commit-queue"
+    help_text = "Patches which should be added to the commit queue"
+    def __init__(self):
+        options = [
+            make_option("--bugs", action="store_true", dest="bugs", help="Output bug links instead of patch links"),
+        ]
+        AbstractDeclarativeCommand.__init__(self, options=options)
+
+    @staticmethod
+    def _needs_commit_queue(patch):
+        if patch.commit_queue() == "+": # If it's already cq+, ignore the patch.
+            log("%s already has cq=%s" % (patch.id(), patch.commit_queue()))
+            return False
+
+        # We only need to worry about patches from contributers who are not yet committers.
+        committer_record = CommitterList().committer_by_email(patch.attacher_email())
+        if committer_record:
+            log("%s committer = %s" % (patch.id(), committer_record))
+        return not committer_record
+
+    def execute(self, options, args, tool):
+        patches = tool.bugs.queries.fetch_patches_from_pending_commit_list()
+        patches_needing_cq = filter(self._needs_commit_queue, patches)
+        if options.bugs:
+            bugs_needing_cq = map(lambda patch: patch.bug_id(), patches_needing_cq)
+            bugs_needing_cq = sorted(set(bugs_needing_cq))
+            for bug_id in bugs_needing_cq:
+                print "%s" % tool.bugs.bug_url_for_bug_id(bug_id)
+        else:
+            for patch in patches_needing_cq:
+                print "%s" % tool.bugs.attachment_url_for_id(patch.id(), action="edit")
+
+
+class PatchesToReview(AbstractDeclarativeCommand):
+    name = "patches-to-review"
+    help_text = "List bugs which have attachments pending review"
+
+    def __init__(self):
+        options = [
+            make_option("--all", action="store_true",
+                        help="Show all bugs regardless of who is on CC (it might take a while)"),
+            make_option("--include-cq-denied", action="store_true",
+                        help="By default, r? patches with cq- are omitted unless this option is set"),
+            make_option("--cc-email",
+                        help="Specifies the email on the CC field (defaults to your bugzilla login email)"),
+        ]
+        AbstractDeclarativeCommand.__init__(self, options=options)
+
+    def _print_report(self, report, cc_email, print_all):
+        if print_all:
+            print "Bugs with attachments pending review:"
+        else:
+            print "Bugs with attachments pending review that has %s in the CC list:" % cc_email
+
+        print "http://webkit.org/b/bugid   Description (age in days)"
+        for row in report:
+            print "%s (%d)" % (row[1], row[0])
+
+        print "Total: %d" % len(report)
+
+    def _generate_report(self, bugs, include_cq_denied):
+        report = []
+
+        for bug in bugs:
+            patch = bug.unreviewed_patches()[-1]
+
+            if not include_cq_denied and patch.commit_queue() == "-":
+                continue
+
+            age_in_days = (datetime.today() - patch.attach_date()).days
+            report.append((age_in_days, "http://webkit.org/b/%-7s %s" % (bug.id(), bug.title())))
+
+        report.sort()
+        return report
+
+    def execute(self, options, args, tool):
+        tool.bugs.authenticate()
+
+        cc_email = options.cc_email
+        if not cc_email and not options.all:
+            cc_email = tool.bugs.username
+
+        bugs = tool.bugs.queries.fetch_bugs_from_review_queue(cc_email=cc_email)
+        report = self._generate_report(bugs, options.include_cq_denied)
+        self._print_report(report, cc_email, options.all)
+
+class WhatBroke(AbstractDeclarativeCommand):
+    name = "what-broke"
+    help_text = "Print failing buildbots (%s) and what revisions broke them" % config_urls.buildbot_url
+
+    def _print_builder_line(self, builder_name, max_name_width, status_message):
+        print "%s : %s" % (builder_name.ljust(max_name_width), status_message)
+
+    def _print_blame_information_for_builder(self, builder_status, name_width, avoid_flakey_tests=True):
+        builder = self._tool.buildbot.builder_with_name(builder_status["name"])
+        red_build = builder.build(builder_status["build_number"])
+        regression_window = builder.find_regression_window(red_build)
+        if not regression_window.failing_build():
+            self._print_builder_line(builder.name(), name_width, "FAIL (error loading build information)")
+            return
+        if not regression_window.build_before_failure():
+            self._print_builder_line(builder.name(), name_width, "FAIL (blame-list: sometime before %s?)" % regression_window.failing_build().revision())
+            return
+
+        revisions = regression_window.revisions()
+        first_failure_message = ""
+        if (regression_window.failing_build() == builder.build(builder_status["build_number"])):
+            first_failure_message = " FIRST FAILURE, possibly a flaky test"
+        self._print_builder_line(builder.name(), name_width, "FAIL (blame-list: %s%s)" % (revisions, first_failure_message))
+        for revision in revisions:
+            commit_info = self._tool.checkout().commit_info_for_revision(revision)
+            if commit_info:
+                print commit_info.blame_string(self._tool.bugs)
+            else:
+                print "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision
+
+    def execute(self, options, args, tool):
+        builder_statuses = tool.buildbot.builder_statuses()
+        longest_builder_name = max(map(len, map(lambda builder: builder["name"], builder_statuses)))
+        failing_builders = 0
+        for builder_status in builder_statuses:
+            # If the builder is green, print OK, exit.
+            if builder_status["is_green"]:
+                continue
+            self._print_blame_information_for_builder(builder_status, name_width=longest_builder_name)
+            failing_builders += 1
+        if failing_builders:
+            print "%s of %s are failing" % (failing_builders, pluralize("builder", len(builder_statuses)))
+        else:
+            print "All builders are passing!"
+
+
+class ResultsFor(AbstractDeclarativeCommand):
+    name = "results-for"
+    help_text = "Print a list of failures for the passed revision from bots on %s" % config_urls.buildbot_url
+    argument_names = "REVISION"
+
+    def _print_layout_test_results(self, results):
+        if not results:
+            print " No results."
+            return
+        for title, files in results.parsed_results().items():
+            print " %s" % title
+            for filename in files:
+                print "  %s" % filename
+
+    def execute(self, options, args, tool):
+        builders = self._tool.buildbot.builders()
+        for builder in builders:
+            print "%s:" % builder.name()
+            build = builder.build_for_revision(args[0], allow_failed_lookups=True)
+            self._print_layout_test_results(build.layout_test_results())
+
+
+class FailureReason(AbstractDeclarativeCommand):
+    name = "failure-reason"
+    help_text = "Lists revisions where individual test failures started at %s" % config_urls.buildbot_url
+
+    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 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)
+        revision_to_test = start_revision
+        build = builder.build_for_revision(revision_to_test, allow_failed_lookups=True)
+        layout_test_results = build.layout_test_results()
+        if not layout_test_results:
+            # FIXME: This could be made more user friendly.
+            print "Failed to load layout test results from %s; can't continue. (start revision = r%s)" % (build.results_url(), start_revision)
+            return 1
+
+        results_to_explain = set(layout_test_results.failing_tests())
+        last_build_with_results = build
+        print "Starting at %s" % revision_to_test
+        while results_to_explain:
+            revision_to_test -= 1
+            new_build = builder.build_for_revision(revision_to_test, allow_failed_lookups=True)
+            if not new_build:
+                print "No build for %s" % revision_to_test
+                continue
+            build = new_build
+            latest_results = build.layout_test_results()
+            if not latest_results:
+                print "No results build %s (r%s)" % (build._number, build.revision())
+                continue
+            failures = set(latest_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())
+                continue
+            fixed_results = results_to_explain - failures
+            if not fixed_results:
+                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
+            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:
+            print "Failed to explain failures: %s" % results_to_explain
+            return 1
+        print "Explained all results for %s" % builder.name()
+        return 0
+
+    def _builder_to_explain(self):
+        builder_statuses = self._tool.buildbot.builder_statuses()
+        red_statuses = [status for status in builder_statuses if not status["is_green"]]
+        print "%s failing" % (pluralize("builder", len(red_statuses)))
+        builder_choices = [status["name"] for status in red_statuses]
+        # We could offer an "All" choice here.
+        chosen_name = self._tool.user.prompt_with_list("Which builder to diagnose:", builder_choices)
+        # FIXME: prompt_with_list should really take a set of objects and a set of names and then return the object.
+        for status in red_statuses:
+            if status["name"] == chosen_name:
+                return (self._tool.buildbot.builder_with_name(chosen_name), status["built_revision"])
+
+    def execute(self, options, args, tool):
+        (builder, latest_revision) = self._builder_to_explain()
+        start_revision = self._tool.user.prompt("Revision to walk backwards from? [%s] " % latest_revision) or latest_revision
+        if not start_revision:
+            print "Revision required."
+            return 1
+        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" % config_urls.buildbot_url
+
+    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 = self._tool.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" % config_urls.buildbot_url
+    long_help = """Fetches build status from http://build.webkit.org/one_box_per_builder
+and displayes the status of each builder."""
+
+    def execute(self, options, args, tool):
+        for builder in tool.buildbot.builder_statuses():
+            status_string = "ok" if builder["is_green"] else "FAIL"
+            print "%s : %s" % (status_string.ljust(4), builder["name"])
+
+
+class CrashLog(AbstractDeclarativeCommand):
+    name = "crash-log"
+    help_text = "Print the newest crash log for the given process"
+    long_help = """Finds the newest crash log matching the given process name
+and PID and prints it to stdout."""
+    argument_names = "PROCESS_NAME [PID]"
+
+    def execute(self, options, args, tool):
+        crash_logs = CrashLogs(tool)
+        pid = None
+        if len(args) > 1:
+            pid = int(args[1])
+        print crash_logs.find_newest_log(args[0], pid)
+
+
+class PrintExpectations(AbstractDeclarativeCommand):
+    name = 'print-expectations'
+    help_text = 'Print the expected result for the given test(s) on the given port(s)'
+
+    def __init__(self):
+        options = [
+            make_option('--all', action='store_true', default=False,
+                        help='display the expectations for *all* tests'),
+            make_option('-x', '--exclude-keyword', action='append', default=[],
+                        help='limit to tests not matching the given keyword (for example, "skip", "slow", or "crash". May specify multiple times'),
+            make_option('-i', '--include-keyword', action='append', default=[],
+                        help='limit to tests with the given keyword (for example, "skip", "slow", or "crash". May specify multiple times'),
+            make_option('--csv', action='store_true', default=False,
+                        help='Print a CSV-style report that includes the port name, modifiers, tests, and expectations'),
+            make_option('-f', '--full', action='store_true', default=False,
+                        help='Print a full TestExpectations-style line for every match'),
+        ] + platform_options(use_globs=True)
+
+        AbstractDeclarativeCommand.__init__(self, options=options)
+        self._expectation_models = {}
+
+    def execute(self, options, args, tool):
+        if not args and not options.all:
+            print "You must either specify one or more test paths or --all."
+            return
+
+        if options.platform:
+            port_names = fnmatch.filter(tool.port_factory.all_port_names(), options.platform)
+            if not port_names:
+                default_port = tool.port_factory.get(options.platform)
+                if default_port:
+                    port_names = [default_port.name()]
+                else:
+                    print "No port names match '%s'" % options.platform
+                    return
+            else:
+                default_port = tool.port_factory.get(port_names[0])
+        else:
+            default_port = tool.port_factory.get(options=options)
+            port_names = [default_port.name()]
+
+        tests = default_port.tests(args)
+        for port_name in port_names:
+            model = self._model(options, port_name, tests)
+            tests_to_print = self._filter_tests(options, model, tests)
+            lines = [model.get_expectation_line(test) for test in sorted(tests_to_print)]
+            if port_name != port_names[0]:
+                print
+            print '\n'.join(self._format_lines(options, port_name, lines))
+
+    def _filter_tests(self, options, model, tests):
+        filtered_tests = set()
+        if options.include_keyword:
+            for keyword in options.include_keyword:
+                filtered_tests.update(model.get_test_set_for_keyword(keyword))
+        else:
+            filtered_tests = tests
+
+        for keyword in options.exclude_keyword:
+            filtered_tests.difference_update(model.get_test_set_for_keyword(keyword))
+        return filtered_tests
+
+    def _format_lines(self, options, port_name, lines):
+        output = []
+        if options.csv:
+            for line in lines:
+                output.append("%s,%s" % (port_name, line.to_csv()))
+        elif lines:
+            include_modifiers = options.full
+            include_expectations = options.full or len(options.include_keyword) != 1 or len(options.exclude_keyword)
+            output.append("// For %s" % port_name)
+            for line in lines:
+                output.append("%s" % line.to_string(None, include_modifiers, include_expectations, include_comment=False))
+        return output
+
+    def _model(self, options, port_name, tests):
+        port = self._tool.port_factory.get(port_name, options)
+        expectations_path = port.path_to_test_expectations_file()
+        if not expectations_path in self._expectation_models:
+            self._expectation_models[expectations_path] = TestExpectations(port, tests).model()
+        return self._expectation_models[expectations_path]
+
+
+class PrintBaselines(AbstractDeclarativeCommand):
+    name = 'print-baselines'
+    help_text = 'Prints the baseline locations for given test(s) on the given port(s)'
+
+    def __init__(self):
+        options = [
+            make_option('--all', action='store_true', default=False,
+                        help='display the baselines for *all* tests'),
+            make_option('--csv', action='store_true', default=False,
+                        help='Print a CSV-style report that includes the port name, test_name, test platform, baseline type, baseline location, and baseline platform'),
+            make_option('--include-virtual-tests', action='store_true',
+                        help='Include virtual tests'),
+        ] + platform_options(use_globs=True)
+        AbstractDeclarativeCommand.__init__(self, options=options)
+        self._platform_regexp = re.compile('platform/([^\/]+)/(.+)')
+
+    def execute(self, options, args, tool):
+        if not args and not options.all:
+            print "You must either specify one or more test paths or --all."
+            return
+
+        default_port = tool.port_factory.get()
+        if options.platform:
+            port_names = fnmatch.filter(tool.port_factory.all_port_names(), options.platform)
+            if not port_names:
+                print "No port names match '%s'" % options.platform
+        else:
+            port_names = [default_port.name()]
+
+        if options.include_virtual_tests:
+            tests = sorted(default_port.tests(args))
+        else:
+            # FIXME: make real_tests() a public method.
+            tests = sorted(default_port._real_tests(args))
+
+        for port_name in port_names:
+            if port_name != port_names[0]:
+                print
+            if not options.csv:
+                print "// For %s" % port_name
+            port = tool.port_factory.get(port_name)
+            for test_name in tests:
+                self._print_baselines(options, port_name, test_name, port.expected_baselines_by_extension(test_name))
+
+    def _print_baselines(self, options, port_name, test_name, baselines):
+        for extension in sorted(baselines.keys()):
+            baseline_location = baselines[extension]
+            if baseline_location:
+                if options.csv:
+                    print "%s,%s,%s,%s,%s,%s" % (port_name, test_name, self._platform_for_path(test_name),
+                                                 extension[1:], baseline_location, self._platform_for_path(baseline_location))
+                else:
+                    print baseline_location
+
+    def _platform_for_path(self, relpath):
+        platform_matchobj = self._platform_regexp.match(relpath)
+        if platform_matchobj:
+            return platform_matchobj.group(1)
+        return None
diff --git a/Tools/Scripts/webkitpy/tool/commands/queries_unittest.py b/Tools/Scripts/webkitpy/tool/commands/queries_unittest.py
new file mode 100644
index 0000000..79bf1ca
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/queries_unittest.py
@@ -0,0 +1,284 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2012 Intel Corporation. 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.system.outputcapture import OutputCapture
+from webkitpy.common.net.bugzilla import Bugzilla
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.layout_tests.port.test import TestPort
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.commands.queries import *
+from webkitpy.tool.mocktool import MockTool, MockOptions
+
+
+class MockTestPort1(object):
+    def skips_layout_test(self, test_name):
+        return test_name in ["media/foo/bar.html", "foo"]
+
+
+class MockTestPort2(object):
+    def skips_layout_test(self, test_name):
+        return test_name == "media/foo/bar.html"
+
+
+class MockPortFactory(object):
+    def __init__(self):
+        self._all_ports = {
+            "test_port1": MockTestPort1(),
+            "test_port2": MockTestPort2(),
+        }
+
+    def all_port_names(self, options=None):
+        return self._all_ports.keys()
+
+    def get(self, port_name):
+        return self._all_ports.get(port_name)
+
+
+class QueryCommandsTest(CommandsTest):
+    def test_bugs_to_commit(self):
+        expected_stderr = "Warning, attachment 10001 on bug 50000 has invalid committer (non-committer@example.com)\n"
+        self.assert_execute_outputs(BugsToCommit(), None, "50000\n50003\n", expected_stderr)
+
+    def test_patches_in_commit_queue(self):
+        expected_stdout = "http://example.com/10000\nhttp://example.com/10002\n"
+        expected_stderr = "Warning, attachment 10001 on bug 50000 has invalid committer (non-committer@example.com)\nPatches in commit queue:\n"
+        self.assert_execute_outputs(PatchesInCommitQueue(), None, expected_stdout, expected_stderr)
+
+    def test_patches_to_commit_queue(self):
+        expected_stdout = "http://example.com/10003&action=edit\n"
+        expected_stderr = "10000 already has cq=+\n10001 already has cq=+\n10004 committer = \"Eric Seidel\" <eric@webkit.org>\n"
+        options = Mock()
+        options.bugs = False
+        self.assert_execute_outputs(PatchesToCommitQueue(), None, expected_stdout, expected_stderr, options=options)
+
+        expected_stdout = "http://example.com/50003\n"
+        options.bugs = True
+        self.assert_execute_outputs(PatchesToCommitQueue(), None, expected_stdout, expected_stderr, options=options)
+
+    def test_patches_to_review(self):
+        options = Mock()
+
+        # When no cc_email is provided, we use the Bugzilla username by default.
+        # The MockBugzilla will fake the authentication using username@webkit.org
+        # as login and it should match the username at the report header.
+        options.cc_email = None
+        options.include_cq_denied = False
+        options.all = False
+        expected_stdout = \
+            "Bugs with attachments pending review that has username@webkit.org in the CC list:\n" \
+            "http://webkit.org/b/bugid   Description (age in days)\n" \
+            "Total: 0\n"
+        expected_stderr = ""
+        self.assert_execute_outputs(PatchesToReview(), None, expected_stdout, expected_stderr, options=options)
+
+        options.cc_email = "abarth@webkit.org"
+        options.include_cq_denied = True
+        options.all = False
+        expected_stdout = \
+            "Bugs with attachments pending review that has abarth@webkit.org in the CC list:\n" \
+            "http://webkit.org/b/bugid   Description (age in days)\n" \
+            "http://webkit.org/b/50001   Bug with a patch needing review. (0)\n" \
+            "Total: 1\n"
+        expected_stderr = ""
+        self.assert_execute_outputs(PatchesToReview(), None, expected_stdout, expected_stderr, options=options)
+
+        options.cc_email = None
+        options.include_cq_denied = True
+        options.all = True
+        expected_stdout = \
+            "Bugs with attachments pending review:\n" \
+            "http://webkit.org/b/bugid   Description (age in days)\n" \
+            "http://webkit.org/b/50001   Bug with a patch needing review. (0)\n" \
+            "Total: 1\n"
+        self.assert_execute_outputs(PatchesToReview(), None, expected_stdout, expected_stderr, options=options)
+
+        options.cc_email = None
+        options.include_cq_denied = False
+        options.all = True
+        expected_stdout = \
+            "Bugs with attachments pending review:\n" \
+            "http://webkit.org/b/bugid   Description (age in days)\n" \
+            "Total: 0\n"
+        self.assert_execute_outputs(PatchesToReview(), None, expected_stdout, expected_stderr, options=options)
+
+        options.cc_email = "invalid_email@example.com"
+        options.all = False
+        options.include_cq_denied = True
+        expected_stdout = \
+            "Bugs with attachments pending review that has invalid_email@example.com in the CC list:\n" \
+            "http://webkit.org/b/bugid   Description (age in days)\n" \
+            "Total: 0\n"
+        self.assert_execute_outputs(PatchesToReview(), None, expected_stdout, expected_stderr, options=options)
+
+    def test_tree_status(self):
+        expected_stdout = "ok   : Builder1\nok   : Builder2\n"
+        self.assert_execute_outputs(TreeStatus(), None, 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(0), "FAILED to fetch CommitInfo for r0, 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(0), "FAILED to fetch CommitInfo for r0, exception: MESSAGE")
+
+
+class PrintExpectationsTest(unittest.TestCase):
+    def run_test(self, tests, expected_stdout, platform='test-win-xp', **args):
+        options = MockOptions(all=False, csv=False, full=False, platform=platform,
+                              include_keyword=[], exclude_keyword=[]).update(**args)
+        tool = MockTool()
+        tool.port_factory.all_port_names = lambda: TestPort.ALL_BASELINE_VARIANTS
+        command = PrintExpectations()
+        command.bind_to_tool(tool)
+
+        oc = OutputCapture()
+        try:
+            oc.capture_output()
+            command.execute(options, tests, tool)
+        finally:
+            stdout, _, _ = oc.restore_output()
+        self.assertEquals(stdout, expected_stdout)
+
+    def test_basic(self):
+        self.run_test(['failures/expected/text.html', 'failures/expected/image.html'],
+                      ('// For test-win-xp\n'
+                       'failures/expected/image.html [ ImageOnlyFailure ]\n'
+                       'failures/expected/text.html [ Failure ]\n'))
+
+    def test_multiple(self):
+        self.run_test(['failures/expected/text.html', 'failures/expected/image.html'],
+                      ('// For test-win-vista\n'
+                       'failures/expected/image.html [ ImageOnlyFailure ]\n'
+                       'failures/expected/text.html [ Failure ]\n'
+                       '\n'
+                       '// For test-win-win7\n'
+                       'failures/expected/image.html [ ImageOnlyFailure ]\n'
+                       'failures/expected/text.html [ Failure ]\n'
+                       '\n'
+                       '// For test-win-xp\n'
+                       'failures/expected/image.html [ ImageOnlyFailure ]\n'
+                       'failures/expected/text.html [ Failure ]\n'),
+                       platform='test-win-*')
+
+    def test_full(self):
+        self.run_test(['failures/expected/text.html', 'failures/expected/image.html'],
+                      ('// For test-win-xp\n'
+                       'Bug(test) failures/expected/image.html [ ImageOnlyFailure ]\n'
+                       'Bug(test) failures/expected/text.html [ Failure ]\n'),
+                      full=True)
+
+    def test_exclude(self):
+        self.run_test(['failures/expected/text.html', 'failures/expected/image.html'],
+                      ('// For test-win-xp\n'
+                       'failures/expected/text.html [ Failure ]\n'),
+                      exclude_keyword=['image'])
+
+    def test_include(self):
+        self.run_test(['failures/expected/text.html', 'failures/expected/image.html'],
+                      ('// For test-win-xp\n'
+                       'failures/expected/image.html\n'),
+                      include_keyword=['image'])
+
+    def test_csv(self):
+        self.run_test(['failures/expected/text.html', 'failures/expected/image.html'],
+                      ('test-win-xp,failures/expected/image.html,BUGTEST,IMAGE\n'
+                       'test-win-xp,failures/expected/text.html,BUGTEST,FAIL\n'),
+                      csv=True)
+
+
+class PrintBaselinesTest(unittest.TestCase):
+    def setUp(self):
+        self.oc = None
+        self.tool = MockTool()
+        self.test_port = self.tool.port_factory.get('test-win-xp')
+        self.tool.port_factory.get = lambda port_name=None: self.test_port
+        self.tool.port_factory.all_port_names = lambda: TestPort.ALL_BASELINE_VARIANTS
+
+    def tearDown(self):
+        if self.oc:
+            self.restore_output()
+
+    def capture_output(self):
+        self.oc = OutputCapture()
+        self.oc.capture_output()
+
+    def restore_output(self):
+        stdout, stderr, logs = self.oc.restore_output()
+        self.oc = None
+        return (stdout, stderr, logs)
+
+    def test_basic(self):
+        command = PrintBaselines()
+        command.bind_to_tool(self.tool)
+        self.capture_output()
+        command.execute(MockOptions(all=False, include_virtual_tests=False, csv=False, platform=None), ['passes/text.html'], self.tool)
+        stdout, _, _ = self.restore_output()
+        self.assertEquals(stdout,
+                          ('// For test-win-xp\n'
+                           'passes/text-expected.png\n'
+                           'passes/text-expected.txt\n'))
+
+    def test_multiple(self):
+        command = PrintBaselines()
+        command.bind_to_tool(self.tool)
+        self.capture_output()
+        command.execute(MockOptions(all=False, include_virtual_tests=False, csv=False, platform='test-win-*'), ['passes/text.html'], self.tool)
+        stdout, _, _ = self.restore_output()
+        self.assertEquals(stdout,
+                          ('// For test-win-vista\n'
+                           'passes/text-expected.png\n'
+                           'passes/text-expected.txt\n'
+                           '\n'
+                           '// For test-win-win7\n'
+                           'passes/text-expected.png\n'
+                           'passes/text-expected.txt\n'
+                           '\n'
+                           '// For test-win-xp\n'
+                           'passes/text-expected.png\n'
+                           'passes/text-expected.txt\n'))
+
+    def test_csv(self):
+        command = PrintBaselines()
+        command.bind_to_tool(self.tool)
+        self.capture_output()
+        command.execute(MockOptions(all=False, platform='*xp', csv=True, include_virtual_tests=False), ['passes/text.html'], self.tool)
+        stdout, _, _ = self.restore_output()
+        self.assertEquals(stdout,
+                          ('test-win-xp,passes/text.html,None,png,passes/text-expected.png,None\n'
+                           'test-win-xp,passes/text.html,None,txt,passes/text-expected.txt,None\n'))
diff --git a/Tools/Scripts/webkitpy/tool/commands/queues.py b/Tools/Scripts/webkitpy/tool/commands/queues.py
new file mode 100644
index 0000000..6993c2d
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/queues.py
@@ -0,0 +1,455 @@
+# Copyright (c) 2009 Google Inc. All rights reserved.
+# Copyright (c) 2009 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:
+# 
+#     * 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 codecs
+import os
+import sys
+import time
+import traceback
+
+from datetime import datetime
+from optparse import make_option
+from StringIO import StringIO
+
+from webkitpy.common.config.committervalidator import CommitterValidator
+from webkitpy.common.config.ports import DeprecatedPort
+from webkitpy.common.net.bugzilla import Attachment
+from webkitpy.common.net.statusserver import StatusServer
+from webkitpy.common.system.deprecated_logging import error, log
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.tool.bot.botinfo import BotInfo
+from webkitpy.tool.bot.commitqueuetask import CommitQueueTask, CommitQueueTaskDelegate
+from webkitpy.tool.bot.expectedfailures import ExpectedFailures
+from webkitpy.tool.bot.feeders import CommitQueueFeeder, EWSFeeder
+from webkitpy.tool.bot.flakytestreporter import FlakyTestReporter
+from webkitpy.tool.bot.layouttestresultsreader import LayoutTestResultsReader
+from webkitpy.tool.bot.patchanalysistask import UnableToApplyPatch
+from webkitpy.tool.bot.queueengine import QueueEngine, QueueEngineDelegate
+from webkitpy.tool.bot.stylequeuetask import StyleQueueTask, StyleQueueTaskDelegate
+from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler
+from webkitpy.tool.multicommandtool import Command, TryAgain
+
+
+class AbstractQueue(Command, QueueEngineDelegate):
+    watchers = [
+    ]
+
+    _pass_status = "Pass"
+    _fail_status = "Fail"
+    _retry_status = "Retry"
+    _error_status = "Error"
+
+    def __init__(self, options=None): # Default values should never be collections (like []) as default values are shared between invocations
+        options_list = (options or []) + [
+            make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue.  Dangerous!"),
+            make_option("--exit-after-iteration", action="store", type="int", dest="iterations", default=None, help="Stop running the queue after iterating this number of times."),
+        ]
+        Command.__init__(self, "Run the %s" % self.name, options=options_list)
+        self._iteration_count = 0
+
+    def _cc_watchers(self, bug_id):
+        try:
+            self._tool.bugs.add_cc_to_bug(bug_id, self.watchers)
+        except Exception, e:
+            traceback.print_exc()
+            log("Failed to CC watchers.")
+
+    def run_webkit_patch(self, args):
+        webkit_patch_args = [self._tool.path()]
+        # FIXME: This is a hack, we should have a more general way to pass global options.
+        # FIXME: We must always pass global options and their value in one argument
+        # because our global option code looks for the first argument which does
+        # not begin with "-" and assumes that is the command name.
+        webkit_patch_args += ["--status-host=%s" % self._tool.status_server.host]
+        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)
+        # FIXME: There is probably no reason to use run_and_throw_if_fail anymore.
+        # run_and_throw_if_fail was invented to support tee'd output
+        # (where we write both to a log file and to the console at once),
+        # but the queues don't need live-progress, a dump-of-output at the
+        # end should be sufficient.
+        return self._tool.executive.run_and_throw_if_fail(webkit_patch_args, cwd=self._tool.scm().checkout_root)
+
+    def _log_directory(self):
+        return os.path.join("..", "%s-logs" % self.name)
+
+    # QueueEngineDelegate methods
+
+    def queue_log_path(self):
+        return os.path.join(self._log_directory(), "%s.log" % self.name)
+
+    def work_item_log_path(self, work_item):
+        raise NotImplementedError, "subclasses must implement"
+
+    def begin_work_queue(self):
+        log("CAUTION: %s will discard all local changes in \"%s\"" % (self.name, self._tool.scm().checkout_root))
+        if self._options.confirm:
+            response = self._tool.user.prompt("Are you sure?  Type \"yes\" to continue: ")
+            if (response != "yes"):
+                error("User declined.")
+        log("Running WebKit %s." % self.name)
+        self._tool.status_server.update_status(self.name, "Starting Queue")
+
+    def stop_work_queue(self, reason):
+        self._tool.status_server.update_status(self.name, "Stopping Queue, reason: %s" % reason)
+
+    def should_continue_work_queue(self):
+        self._iteration_count += 1
+        return not self._options.iterations or self._iteration_count <= self._options.iterations
+
+    def next_work_item(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def process_work_item(self, work_item):
+        raise NotImplementedError, "subclasses must implement"
+
+    def handle_unexpected_error(self, work_item, message):
+        raise NotImplementedError, "subclasses must implement"
+
+    # 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._tool = tool  # FIXME: This code is wrong too!  Command.bind_to_tool handles this!
+        return engine(self.name, self, self._tool.wakeup_event).run()
+
+    @classmethod
+    def _log_from_script_error_for_upload(cls, script_error, output_limit=None):
+        # We have seen request timeouts with app engine due to large
+        # log uploads.  Trying only the last 512k.
+        if not output_limit:
+            output_limit = 512 * 1024  # 512k
+        output = script_error.message_with_output(output_limit=output_limit)
+        # We pre-encode the string to a byte array before passing it
+        # to status_server, because ClientForm (part of mechanize)
+        # wants a file-like object with pre-encoded data.
+        return StringIO(output.encode("utf-8"))
+
+    @classmethod
+    def _update_status_for_script_error(cls, tool, state, script_error, is_error=False):
+        message = str(script_error)
+        if is_error:
+            message = "Error: %s" % message
+        failure_log = cls._log_from_script_error_for_upload(script_error)
+        return tool.status_server.update_status(cls.name, message, state["patch"], failure_log)
+
+
+class FeederQueue(AbstractQueue):
+    name = "feeder-queue"
+
+    _sleep_duration = 30  # seconds
+
+    # AbstractQueue methods
+
+    def begin_work_queue(self):
+        AbstractQueue.begin_work_queue(self)
+        self.feeders = [
+            CommitQueueFeeder(self._tool),
+            EWSFeeder(self._tool),
+        ]
+
+    def next_work_item(self):
+        # This really show inherit from some more basic class that doesn't
+        # understand work items, but the base class in the heirarchy currently
+        # understands work items.
+        return "synthetic-work-item"
+
+    def process_work_item(self, work_item):
+        for feeder in self.feeders:
+            feeder.feed()
+        time.sleep(self._sleep_duration)
+        return True
+
+    def work_item_log_path(self, work_item):
+        return None
+
+    def handle_unexpected_error(self, work_item, message):
+        log(message)
+
+
+class AbstractPatchQueue(AbstractQueue):
+    def _update_status(self, message, patch=None, results_file=None):
+        return self._tool.status_server.update_status(self.name, message, patch, results_file)
+
+    def _next_patch(self):
+        # FIXME: Bugzilla accessibility should be checked here; if it's unaccessible,
+        # it should return None.
+        patch = None
+        while not patch:
+            patch_id = self._tool.status_server.next_work_item(self.name)
+            if not patch_id:
+                return None
+            patch = self._tool.bugs.fetch_attachment(patch_id)
+            if not patch:
+                # FIXME: Using a fake patch because release_work_item has the wrong API.
+                # We also don't really need to release the lock (although that's fine),
+                # mostly we just need to remove this bogus patch from our queue.
+                # If for some reason bugzilla is just down, then it will be re-fed later.
+                fake_patch = Attachment({'id': patch_id}, None)
+                self._release_work_item(fake_patch)
+        return patch
+
+    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)
+        self._release_work_item(patch)
+
+    def _did_fail(self, patch):
+        self._update_status(self._fail_status, patch)
+        self._release_work_item(patch)
+
+    def _did_retry(self, patch):
+        self._update_status(self._retry_status, patch)
+        self._release_work_item(patch)
+
+    def _did_error(self, patch, reason):
+        message = "%s: %s" % (self._error_status, reason)
+        self._update_status(message, patch)
+        self._release_work_item(patch)
+
+    # FIXME: This probably belongs at a layer below AbstractPatchQueue, but shared by CommitQueue and the EarlyWarningSystem.
+    def _upload_results_archive_for_patch(self, patch, results_archive_zip):
+        bot_id = self._tool.status_server.bot_id or "bot"
+        description = "Archive of layout-test-results from %s" % bot_id
+        # results_archive is a ZipFile object, grab the File object (.fp) to pass to Mechanize for uploading.
+        results_archive_file = results_archive_zip.fp
+        # Rewind the file object to start (since Mechanize won't do that automatically)
+        # See https://bugs.webkit.org/show_bug.cgi?id=54593
+        results_archive_file.seek(0)
+        # FIXME: This is a small lie to always say run-webkit-tests since Chromium uses new-run-webkit-tests.
+        # We could make this code look up the test script name off the port.
+        comment_text = "The attached test failures were seen while running run-webkit-tests on the %s.\n" % (self.name)
+        # FIXME: We could easily list the test failures from the archive here,
+        # currently callers do that separately.
+        comment_text += BotInfo(self._tool).summary_text()
+        self._tool.bugs.add_attachment_to_bug(patch.bug_id(), results_archive_file, description, filename="layout-test-results.zip", comment_text=comment_text)
+
+    def work_item_log_path(self, patch):
+        return os.path.join(self._log_directory(), "%s.log" % patch.bug_id())
+
+
+class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler, CommitQueueTaskDelegate):
+    name = "commit-queue"
+    port_name = "chromium-xvfb"
+
+    def __init__(self):
+        AbstractPatchQueue.__init__(self)
+        self.port = DeprecatedPort.port(self.port_name)
+
+    # AbstractPatchQueue methods
+
+    def begin_work_queue(self):
+        # FIXME: This violates abstraction
+        self._tool._deprecated_port = self.port
+        AbstractPatchQueue.begin_work_queue(self)
+        self.committer_validator = CommitterValidator(self._tool)
+        self._expected_failures = ExpectedFailures()
+        self._layout_test_results_reader = LayoutTestResultsReader(self._tool, self._log_directory())
+
+    def next_work_item(self):
+        return self._next_patch()
+
+    def process_work_item(self, patch):
+        self._cc_watchers(patch.bug_id())
+        task = CommitQueueTask(self, patch)
+        try:
+            if task.run():
+                self._did_pass(patch)
+                return True
+            self._did_retry(patch)
+        except ScriptError, e:
+            validator = CommitterValidator(self._tool)
+            validator.reject_patch_from_commit_queue(patch.id(), self._error_message_for_bug(task, patch, e))
+            results_archive = task.results_archive_from_patch_test_run(patch)
+            if results_archive:
+                self._upload_results_archive_for_patch(patch, results_archive)
+            self._did_fail(patch)
+
+    def _failing_tests_message(self, task, patch):
+        results = task.results_from_patch_test_run(patch)
+        unexpected_failures = self._expected_failures.unexpected_failures_observed(results)
+        if not unexpected_failures:
+            return None
+        return "New failing tests:\n%s" % "\n".join(unexpected_failures)
+
+    def _error_message_for_bug(self, task, patch, script_error):
+        message = self._failing_tests_message(task, patch)
+        if not message:
+            message = script_error.message_with_output()
+        results_link = self._tool.status_server.results_url_for_status(task.failure_status_id)
+        return "%s\nFull output: %s" % (message, 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 + [self.port.flag()])
+
+    def command_passed(self, message, patch):
+        self._update_status(message, patch=patch)
+
+    def command_failed(self, message, script_error, patch):
+        failure_log = self._log_from_script_error_for_upload(script_error)
+        return self._update_status(message, patch=patch, results_file=failure_log)
+
+    def expected_failures(self):
+        return self._expected_failures
+
+    def test_results(self):
+        return self._layout_test_results_reader.results()
+
+    def archive_last_test_results(self, patch):
+        return self._layout_test_results_reader.archive(patch)
+
+    def build_style(self):
+        return "release"
+
+    def refetch_patch(self, patch):
+        return self._tool.bugs.fetch_attachment(patch.id())
+
+    def report_flaky_tests(self, patch, flaky_test_results, results_archive=None):
+        reporter = FlakyTestReporter(self._tool, self.name)
+        reporter.report_flaky_tests(patch, flaky_test_results, results_archive)
+
+    def did_pass_testing_ews(self, patch):
+        # Currently, chromium-ews is the only testing EWS. Once there are more,
+        # should make sure they all pass.
+        status = self._tool.status_server.patch_status("chromium-ews", patch.id())
+        return status == self._pass_status
+
+    # StepSequenceErrorHandler methods
+
+    def handle_script_error(cls, tool, state, script_error):
+        # Hitting this error handler should be pretty rare.  It does occur,
+        # however, when a patch no longer applies to top-of-tree in the final
+        # land step.
+        log(script_error.message_with_output())
+
+    @classmethod
+    def handle_checkout_needs_update(cls, tool, state, options, error):
+        message = "Tests passed, but commit failed (checkout out of date).  Updating, then landing without building or re-running tests."
+        tool.status_server.update_status(cls.name, message, state["patch"])
+        # The only time when we find out that out checkout needs update is
+        # when we were ready to actually pull the trigger and land the patch.
+        # Rather than spinning in the master process, we retry without
+        # building or testing, which is much faster.
+        options.build = False
+        options.test = False
+        options.update = True
+        raise TryAgain()
+
+
+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")
+
+    # AbstractPatchQueue methods
+
+    def begin_work_queue(self):
+        AbstractPatchQueue.begin_work_queue(self)
+
+    def next_work_item(self):
+        return self._next_patch()
+
+    def process_work_item(self, patch):
+        try:
+            if not self.review_patch(patch):
+                return False
+            self._did_pass(patch)
+            return True
+        except ScriptError, e:
+            if e.exit_code != QueueEngine.handled_error_code:
+                self._did_fail(patch)
+            else:
+                # The subprocess handled the error, but won't have released the patch, so we do.
+                # FIXME: We need to simplify the rules by which _release_work_item is called.
+                self._release_work_item(patch)
+            raise e
+
+    def handle_unexpected_error(self, patch, message):
+        log(message)
+
+    # StepSequenceErrorHandler methods
+
+    @classmethod
+    def handle_script_error(cls, tool, state, script_error):
+        log(script_error.output)
+
+
+class StyleQueue(AbstractReviewQueue, StyleQueueTaskDelegate):
+    name = "style-queue"
+
+    def __init__(self):
+        AbstractReviewQueue.__init__(self)
+
+    def review_patch(self, patch):
+        task = StyleQueueTask(self, patch)
+        if not task.validate():
+            self._did_error(patch, "%s did not process patch." % self.name)
+            return False
+        try:
+            return task.run()
+        except UnableToApplyPatch, e:
+            self._did_error(patch, "%s unable to apply patch." % self.name)
+            return False
+        except ScriptError, e:
+            message = "Attachment %s did not pass %s:\n\n%s\n\nIf any of these errors are false positives, please file a bug against check-webkit-style." % (patch.id(), self.name, e.output)
+            self._tool.bugs.post_comment_to_bug(patch.bug_id(), message, cc=self.watchers)
+            self._did_fail(patch)
+            return False
+        return True
+
+    # StyleQueueTaskDelegate methods
+
+    def run_command(self, command):
+        self.run_webkit_patch(command)
+
+    def command_passed(self, message, patch):
+        self._update_status(message, patch=patch)
+
+    def command_failed(self, message, script_error, patch):
+        failure_log = self._log_from_script_error_for_upload(script_error)
+        return self._update_status(message, patch=patch, results_file=failure_log)
+
+    def expected_failures(self):
+        return None
+
+    def refetch_patch(self, patch):
+        return self._tool.bugs.fetch_attachment(patch.id())
diff --git a/Tools/Scripts/webkitpy/tool/commands/queues_unittest.py b/Tools/Scripts/webkitpy/tool/commands/queues_unittest.py
new file mode 100644
index 0000000..6301fea
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/queues_unittest.py
@@ -0,0 +1,496 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import StringIO
+
+from webkitpy.common.checkout.scm import CheckoutNeedsUpdate
+from webkitpy.common.checkout.scm.scm_mock import MockSCM
+from webkitpy.common.net.bugzilla import Attachment
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.layout_tests.models import test_results
+from webkitpy.layout_tests.models import test_failures
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.commands.queues import *
+from webkitpy.tool.commands.queuestest import QueuesTest
+from webkitpy.tool.commands.stepsequence import StepSequence
+from webkitpy.common.net.statusserver_mock import MockStatusServer
+from webkitpy.tool.mocktool import MockTool, MockOptions
+
+
+class TestCommitQueue(CommitQueue):
+    def __init__(self, tool=None):
+        CommitQueue.__init__(self)
+        if tool:
+            self.bind_to_tool(tool)
+        self._options = MockOptions(confirm=False, parent_command="commit-queue", port=None)
+
+    def begin_work_queue(self):
+        output_capture = OutputCapture()
+        output_capture.capture_output()
+        CommitQueue.begin_work_queue(self)
+        output_capture.restore_output()
+
+
+class TestQueue(AbstractPatchQueue):
+    name = "test-queue"
+
+
+class TestReviewQueue(AbstractReviewQueue):
+    name = "test-review-queue"
+
+
+class TestFeederQueue(FeederQueue):
+    _sleep_duration = 0
+
+
+class AbstractQueueTest(CommandsTest):
+    def test_log_directory(self):
+        self.assertEquals(TestQueue()._log_directory(), os.path.join("..", "test-queue-logs"))
+
+    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", "--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, cwd='/mock-checkout')
+
+    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
+        self.assertTrue(queue.should_continue_work_queue())
+        self.assertTrue(queue.should_continue_work_queue())
+        self.assertTrue(queue.should_continue_work_queue())
+        self.assertFalse(queue.should_continue_work_queue())
+
+    def test_no_iteration_count(self):
+        queue = TestQueue()
+        queue._options = Mock()
+        self.assertTrue(queue.should_continue_work_queue())
+        self.assertTrue(queue.should_continue_work_queue())
+        self.assertTrue(queue.should_continue_work_queue())
+        self.assertTrue(queue.should_continue_work_queue())
+
+    def _assert_log_message(self, script_error, log_message):
+        failure_log = AbstractQueue._log_from_script_error_for_upload(script_error, output_limit=10)
+        self.assertTrue(failure_log.read(), log_message)
+
+    def test_log_from_script_error_for_upload(self):
+        self._assert_log_message(ScriptError("test"), "test")
+        unicode_tor = u"WebKit \u2661 Tor Arne Vestb\u00F8!"
+        utf8_tor = unicode_tor.encode("utf-8")
+        self._assert_log_message(ScriptError(unicode_tor), utf8_tor)
+        script_error = ScriptError(unicode_tor, output=unicode_tor)
+        expected_output = "%s\nLast %s characters of output:\n%s" % (utf8_tor, 10, utf8_tor[-10:])
+        self._assert_log_message(script_error, expected_output)
+
+
+class FeederQueueTest(QueuesTest):
+    def test_feeder_queue(self):
+        queue = TestFeederQueue()
+        tool = MockTool(log_executive=True)
+        expected_stderr = {
+            "begin_work_queue": self._default_begin_work_queue_stderr("feeder-queue"),
+            "next_work_item": "",
+            "process_work_item": """Warning, attachment 10001 on bug 50000 has invalid committer (non-committer@example.com)
+Warning, attachment 10001 on bug 50000 has invalid committer (non-committer@example.com)
+MOCK setting flag 'commit-queue' to '-' on attachment '10001' with comment 'Rejecting attachment 10001 from commit-queue.' and additional comment 'non-committer@example.com does not have committer permissions according to http://trac.webkit.org/browser/trunk/Tools/Scripts/webkitpy/common/config/committers.py.
+
+- If you do not have committer rights please read http://webkit.org/coding/contributing.html for instructions on how to use bugzilla flags.
+
+- If you have committer rights please correct the error in Tools/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 [10005, 10000]
+Feeding commit-queue items [10005, 10000]
+Feeding EWS (1 r? patch, 1 new)
+MOCK: submit_to_ews: 10002
+""",
+            "handle_unexpected_error": "Mock error message\n",
+        }
+        self.assert_queue_outputs(queue, tool=tool, expected_stderr=expected_stderr)
+
+
+class AbstractPatchQueueTest(CommandsTest):
+    def test_next_patch(self):
+        queue = AbstractPatchQueue()
+        tool = MockTool()
+        queue.bind_to_tool(tool)
+        queue._options = Mock()
+        queue._options.port = None
+        self.assertEquals(queue._next_patch(), None)
+        tool.status_server = MockStatusServer(work_items=[2, 10000, 10001])
+        expected_stdout = "MOCK: fetch_attachment: 2 is not a known attachment id\n"  # A mock-only message to prevent us from making mistakes.
+        expected_stderr = "MOCK: release_work_item: None 2\n"
+        patch = OutputCapture().assert_outputs(self, queue._next_patch, expected_stdout=expected_stdout, expected_stderr=expected_stderr)
+        # The patch.id() == 2 is ignored because it doesn't exist.
+        self.assertEquals(patch.id(), 10000)
+        self.assertEquals(queue._next_patch().id(), 10001)
+        self.assertEquals(queue._next_patch(), None)    # When the queue is empty
+
+    def test_upload_results_archive_for_patch(self):
+        queue = AbstractPatchQueue()
+        queue.name = "mock-queue"
+        tool = MockTool()
+        queue.bind_to_tool(tool)
+        queue._options = Mock()
+        queue._options.port = None
+        patch = queue._tool.bugs.fetch_attachment(10001)
+        expected_stderr = """MOCK add_attachment_to_bug: bug_id=50000, description=Archive of layout-test-results from bot filename=layout-test-results.zip mimetype=None
+-- Begin comment --
+The attached test failures were seen while running run-webkit-tests on the mock-queue.
+Port: MockPort  Platform: MockPlatform 1.0
+-- End comment --
+"""
+        OutputCapture().assert_outputs(self, queue._upload_results_archive_for_patch, [patch, Mock()], expected_stderr=expected_stderr)
+
+
+class NeedsUpdateSequence(StepSequence):
+    def _run(self, tool, options, state):
+        raise CheckoutNeedsUpdate([], 1, "", None)
+
+
+class AlwaysCommitQueueTool(object):
+    def __init__(self):
+        self.status_server = MockStatusServer()
+
+    def command_by_name(self, name):
+        return CommitQueue
+
+
+class SecondThoughtsCommitQueue(TestCommitQueue):
+    def __init__(self, tool=None):
+        self._reject_patch = False
+        TestCommitQueue.__init__(self, tool)
+
+    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(),
+            "name": "Rejected",
+            "is_obsolete": True,
+            "is_patch": False,
+            "review": "-",
+            "reviewer_email": "foo@bar.com",
+            "commit-queue": "-",
+            "committer_email": "foo@bar.com",
+            "attacher_email": "Contributer1",
+        }
+        return Attachment(attachment_dictionary, None)
+
+
+class CommitQueueTest(QueuesTest):
+    def _mock_test_result(self, testname):
+        return test_results.TestResult(testname, [test_failures.FailureTextMismatch()])
+
+    def test_commit_queue(self):
+        tool = MockTool()
+        tool.filesystem.write_text_file('/tmp/layout-test-results/full_results.json', '')  # Otherwise the commit-queue will hit a KeyError trying to read the results from the MockFileSystem.
+        tool.filesystem.write_text_file('/tmp/layout-test-results/webkit_unit_tests_output.xml', '')
+        expected_stderr = {
+            "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue"),
+            "next_work_item": "",
+            "process_work_item": """MOCK: update_status: commit-queue Cleaned working directory
+MOCK: update_status: commit-queue Updated working directory
+MOCK: update_status: commit-queue Applied patch
+MOCK: update_status: commit-queue ChangeLog validated
+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: release_work_item: commit-queue 10000
+""",
+            "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'Mock error message'\n",
+            "handle_script_error": "ScriptError error message\n\nMOCK output\n",
+        }
+        self.assert_queue_outputs(CommitQueue(), tool=tool, expected_stderr=expected_stderr)
+
+    def test_commit_queue_failure(self):
+        expected_stderr = {
+            "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue"),
+            "next_work_item": "",
+            "process_work_item": """MOCK: update_status: commit-queue Cleaned working directory
+MOCK: update_status: commit-queue Updated working directory
+MOCK: update_status: commit-queue Patch does not apply
+MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'MOCK script error
+Full output: http://dummy_url'
+MOCK: update_status: commit-queue Fail
+MOCK: release_work_item: commit-queue 10000
+""",
+            "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'Mock error message'\n",
+            "handle_script_error": "ScriptError error message\n\nMOCK output\n",
+        }
+        queue = CommitQueue()
+
+        def mock_run_webkit_patch(command):
+            if command[0] == 'clean' or command[0] == 'update':
+                # We want cleaning to succeed so we can error out on a step
+                # that causes the commit-queue to reject the patch.
+                return
+            raise ScriptError('MOCK script error')
+
+        queue.run_webkit_patch = mock_run_webkit_patch
+        self.assert_queue_outputs(queue, expected_stderr=expected_stderr)
+
+    def test_commit_queue_failure_with_failing_tests(self):
+        expected_stderr = {
+            "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue"),
+            "next_work_item": "",
+            "process_work_item": """MOCK: update_status: commit-queue Cleaned working directory
+MOCK: update_status: commit-queue Updated working directory
+MOCK: update_status: commit-queue Patch does not apply
+MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'New failing tests:
+mock_test_name.html
+another_test_name.html
+Full output: http://dummy_url'
+MOCK: update_status: commit-queue Fail
+MOCK: release_work_item: commit-queue 10000
+""",
+            "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'Mock error message'\n",
+            "handle_script_error": "ScriptError error message\n\nMOCK output\n",
+        }
+        queue = CommitQueue()
+
+        def mock_run_webkit_patch(command):
+            if command[0] == 'clean' or command[0] == 'update':
+                # We want cleaning to succeed so we can error out on a step
+                # that causes the commit-queue to reject the patch.
+                return
+            queue._expected_failures.unexpected_failures_observed = lambda results: ["mock_test_name.html", "another_test_name.html"]
+            raise ScriptError('MOCK script error')
+
+        queue.run_webkit_patch = mock_run_webkit_patch
+        self.assert_queue_outputs(queue, expected_stderr=expected_stderr)
+
+    def test_rollout(self):
+        tool = MockTool(log_executive=True)
+        tool.filesystem.write_text_file('/tmp/layout-test-results/full_results.json', '')  # Otherwise the commit-queue will hit a KeyError trying to read the results from the MockFileSystem.
+        tool.filesystem.write_text_file('/tmp/layout-test-results/webkit_unit_tests_output.xml', '')
+        tool.buildbot.light_tree_on_fire()
+        expected_stderr = {
+            "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue"),
+            "next_work_item": "",
+            "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'clean', '--port=%(port_name)s'], cwd=/mock-checkout
+MOCK: update_status: commit-queue Cleaned working directory
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'update', '--port=%(port_name)s'], cwd=/mock-checkout
+MOCK: update_status: commit-queue Updated working directory
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-attachment', '--no-update', '--non-interactive', 10000, '--port=%(port_name)s'], cwd=/mock-checkout
+MOCK: update_status: commit-queue Applied patch
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'validate-changelog', '--non-interactive', 10000, '--port=%(port_name)s'], cwd=/mock-checkout
+MOCK: update_status: commit-queue ChangeLog validated
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build', '--no-clean', '--no-update', '--build-style=release', '--port=%(port_name)s'], cwd=/mock-checkout
+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', '--non-interactive', '--port=%(port_name)s'], cwd=/mock-checkout
+MOCK: update_status: commit-queue Passed tests
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'land-attachment', '--force-clean', '--non-interactive', '--parent-command=commit-queue', 10000, '--port=%(port_name)s'], cwd=/mock-checkout
+MOCK: update_status: commit-queue Landed patch
+MOCK: update_status: commit-queue Pass
+MOCK: release_work_item: commit-queue 10000
+""" % {"port_name": CommitQueue.port_name},
+            "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'Mock error message'\n",
+            "handle_script_error": "ScriptError error message\n\nMOCK output\n",
+        }
+        self.assert_queue_outputs(CommitQueue(), tool=tool, expected_stderr=expected_stderr)
+
+    def test_rollout_lands(self):
+        tool = MockTool(log_executive=True)
+        tool.buildbot.light_tree_on_fire()
+        rollout_patch = tool.bugs.fetch_attachment(10005)  # _patch6, a rollout patch.
+        assert(rollout_patch.is_rollout())
+        expected_stderr = {
+            "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue"),
+            "next_work_item": "",
+            "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'clean', '--port=%(port_name)s'], cwd=/mock-checkout
+MOCK: update_status: commit-queue Cleaned working directory
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'update', '--port=%(port_name)s'], cwd=/mock-checkout
+MOCK: update_status: commit-queue Updated working directory
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-attachment', '--no-update', '--non-interactive', 10005, '--port=%(port_name)s'], cwd=/mock-checkout
+MOCK: update_status: commit-queue Applied patch
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'validate-changelog', '--non-interactive', 10005, '--port=%(port_name)s'], cwd=/mock-checkout
+MOCK: update_status: commit-queue ChangeLog validated
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'land-attachment', '--force-clean', '--non-interactive', '--parent-command=commit-queue', 10005, '--port=%(port_name)s'], cwd=/mock-checkout
+MOCK: update_status: commit-queue Landed patch
+MOCK: update_status: commit-queue Pass
+MOCK: release_work_item: commit-queue 10005
+""" % {"port_name": CommitQueue.port_name},
+            "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '10005' with comment 'Rejecting attachment 10005 from commit-queue.' and additional comment 'Mock error message'\n",
+            "handle_script_error": "ScriptError error message\n\nMOCK output\n",
+        }
+        self.assert_queue_outputs(CommitQueue(), tool=tool, work_item=rollout_patch, expected_stderr=expected_stderr)
+
+    def test_auto_retry(self):
+        queue = CommitQueue()
+        options = Mock()
+        options.parent_command = "commit-queue"
+        tool = AlwaysCommitQueueTool()
+        sequence = NeedsUpdateSequence(None)
+
+        expected_stderr = "Commit failed because the checkout is out of date.  Please update and try again.\nMOCK: update_status: commit-queue Tests passed, but commit failed (checkout out of date).  Updating, then landing without building or re-running tests.\n"
+        state = {'patch': None}
+        OutputCapture().assert_outputs(self, sequence.run_and_handle_errors, [tool, options, state], expected_exception=TryAgain, expected_stderr=expected_stderr)
+
+        self.assertEquals(options.update, True)
+        self.assertEquals(options.build, False)
+        self.assertEquals(options.test, False)
+
+    def test_manual_reject_during_processing(self):
+        queue = SecondThoughtsCommitQueue(MockTool())
+        queue.begin_work_queue()
+        queue._tool.filesystem.write_text_file('/tmp/layout-test-results/full_results.json', '')  # Otherwise the commit-queue will hit a KeyError trying to read the results from the MockFileSystem.
+        queue._tool.filesystem.write_text_file('/tmp/layout-test-results/webkit_unit_tests_output.xml', '')
+        queue._options = Mock()
+        queue._options.port = None
+        expected_stderr = """MOCK: update_status: commit-queue Cleaned working directory
+MOCK: update_status: commit-queue Updated working directory
+MOCK: update_status: commit-queue Applied patch
+MOCK: update_status: commit-queue ChangeLog validated
+MOCK: update_status: commit-queue Built patch
+MOCK: update_status: commit-queue Passed tests
+MOCK: update_status: commit-queue Retry
+MOCK: release_work_item: commit-queue 10000
+"""
+        OutputCapture().assert_outputs(self, queue.process_work_item, [QueuesTest.mock_work_item], expected_stderr=expected_stderr)
+
+    def test_report_flaky_tests(self):
+        queue = TestCommitQueue(MockTool())
+        expected_stderr = """MOCK bug comment: bug_id=50002, cc=None
+--- Begin comment ---
+The commit-queue just saw foo/bar.html flake (text diff) while processing attachment 10000 on bug 50000.
+Port: MockPort  Platform: MockPlatform 1.0
+--- End comment ---
+
+MOCK add_attachment_to_bug: bug_id=50002, description=Failure diff from bot filename=failure.diff mimetype=None
+MOCK bug comment: bug_id=50002, cc=None
+--- Begin comment ---
+The commit-queue just saw bar/baz.html flake (text diff) while processing attachment 10000 on bug 50000.
+Port: MockPort  Platform: MockPlatform 1.0
+--- End comment ---
+
+MOCK add_attachment_to_bug: bug_id=50002, description=Archive of layout-test-results from bot filename=layout-test-results.zip mimetype=None
+MOCK bug comment: bug_id=50000, cc=None
+--- Begin comment ---
+The commit-queue encountered the following flaky tests while processing attachment 10000:
+
+foo/bar.html bug 50002 (author: abarth@webkit.org)
+bar/baz.html bug 50002 (author: abarth@webkit.org)
+The commit-queue is continuing to process your patch.
+--- End comment ---
+
+"""
+        test_names = ["foo/bar.html", "bar/baz.html"]
+        test_results = [self._mock_test_result(name) for name in test_names]
+
+        class MockZipFile(object):
+            def __init__(self):
+                self.fp = StringIO()
+
+            def read(self, path):
+                return ""
+
+            def namelist(self):
+                # This is intentionally missing one diffs.txt to exercise the "upload the whole zip" codepath.
+                return ['foo/bar-diffs.txt']
+
+        OutputCapture().assert_outputs(self, queue.report_flaky_tests, [QueuesTest.mock_work_item, test_results, MockZipFile()], expected_stderr=expected_stderr)
+
+    def test_did_pass_testing_ews(self):
+        tool = MockTool()
+        patch = tool.bugs.fetch_attachment(10000)
+        queue = TestCommitQueue(tool)
+        self.assertFalse(queue.did_pass_testing_ews(patch))
+
+
+class StyleQueueTest(QueuesTest):
+    def test_style_queue_with_style_exception(self):
+        expected_stderr = {
+            "begin_work_queue": self._default_begin_work_queue_stderr("style-queue"),
+            "next_work_item": "",
+            "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'clean'], cwd=/mock-checkout
+MOCK: update_status: style-queue Cleaned working directory
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'update'], cwd=/mock-checkout
+MOCK: update_status: style-queue Updated working directory
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-attachment', '--no-update', '--non-interactive', 10000], cwd=/mock-checkout
+MOCK: update_status: style-queue Applied patch
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-watchlist-local', 50000], cwd=/mock-checkout
+MOCK: update_status: style-queue Watchlist applied
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'check-style-local', '--non-interactive', '--quiet'], cwd=/mock-checkout
+MOCK: update_status: style-queue Style checked
+MOCK: update_status: style-queue Pass
+MOCK: release_work_item: style-queue 10000
+""",
+            "handle_unexpected_error": "Mock error message\n",
+            "handle_script_error": "MOCK output\n",
+        }
+        tool = MockTool(log_executive=True, executive_throws_when_run=set(['check-style']))
+        self.assert_queue_outputs(StyleQueue(), expected_stderr=expected_stderr, tool=tool)
+
+    def test_style_queue_with_watch_list_exception(self):
+        expected_stderr = {
+            "begin_work_queue": self._default_begin_work_queue_stderr("style-queue"),
+            "next_work_item": "",
+            "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'clean'], cwd=/mock-checkout
+MOCK: update_status: style-queue Cleaned working directory
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'update'], cwd=/mock-checkout
+MOCK: update_status: style-queue Updated working directory
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-attachment', '--no-update', '--non-interactive', 10000], cwd=/mock-checkout
+MOCK: update_status: style-queue Applied patch
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-watchlist-local', 50000], cwd=/mock-checkout
+MOCK: update_status: style-queue Unabled to apply watchlist
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'check-style-local', '--non-interactive', '--quiet'], cwd=/mock-checkout
+MOCK: update_status: style-queue Style checked
+MOCK: update_status: style-queue Pass
+MOCK: release_work_item: style-queue 10000
+""",
+            "handle_unexpected_error": "Mock error message\n",
+            "handle_script_error": "MOCK output\n",
+        }
+        tool = MockTool(log_executive=True, executive_throws_when_run=set(['apply-watchlist-local']))
+        self.assert_queue_outputs(StyleQueue(), expected_stderr=expected_stderr, tool=tool)
diff --git a/Tools/Scripts/webkitpy/tool/commands/queuestest.py b/Tools/Scripts/webkitpy/tool/commands/queuestest.py
new file mode 100644
index 0000000..b99302c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/queuestest.py
@@ -0,0 +1,98 @@
+# Copyright (C) 2009 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.bugzilla import Attachment
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler
+from webkitpy.tool.mocktool import MockTool
+
+
+class MockQueueEngine(object):
+    def __init__(self, name, queue, wakeup_event):
+        pass
+
+    def run(self):
+        pass
+
+
+class QueuesTest(unittest.TestCase):
+    # This is _patch1 in mocktool.py
+    mock_work_item = MockTool().bugs.fetch_attachment(10000)
+
+    def assert_outputs(self, func, func_name, args, expected_stdout, expected_stderr, expected_exceptions):
+        exception = None
+        if expected_exceptions and func_name in expected_exceptions:
+            exception = expected_exceptions[func_name]
+
+        OutputCapture().assert_outputs(self,
+                func,
+                args=args,
+                expected_stdout=expected_stdout.get(func_name, ""),
+                expected_stderr=expected_stderr.get(func_name, ""),
+                expected_exception=exception)
+
+    def _default_begin_work_queue_stderr(self, name):
+        checkout_dir = '/mock-checkout'
+        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=None, tool=None):
+        if not tool:
+            tool = MockTool()
+            # This is a hack to make it easy for callers to not have to setup a custom MockFileSystem just to test the commit-queue
+            # the cq tries to read the layout test results, and will hit a KeyError in MockFileSystem if we don't do this.
+            tool.filesystem.write_text_file('/mock-results/results.html', "")
+        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"
+
+        queue.execute(options, args, tool, engine=MockQueueEngine)
+
+        self.assert_outputs(queue.queue_log_path, "queue_log_path", [], expected_stdout, expected_stderr, expected_exceptions)
+        self.assert_outputs(queue.work_item_log_path, "work_item_log_path", [work_item], expected_stdout, expected_stderr, expected_exceptions)
+        self.assert_outputs(queue.begin_work_queue, "begin_work_queue", [], expected_stdout, expected_stderr, expected_exceptions)
+        self.assert_outputs(queue.should_continue_work_queue, "should_continue_work_queue", [], expected_stdout, expected_stderr, expected_exceptions)
+        self.assert_outputs(queue.next_work_item, "next_work_item", [], expected_stdout, expected_stderr, expected_exceptions)
+        self.assert_outputs(queue.process_work_item, "process_work_item", [work_item], expected_stdout, expected_stderr, expected_exceptions)
+        self.assert_outputs(queue.handle_unexpected_error, "handle_unexpected_error", [work_item, "Mock error message"], expected_stdout, expected_stderr, expected_exceptions)
+        # Should we have a different function for testing StepSequenceErrorHandlers?
+        if isinstance(queue, StepSequenceErrorHandler):
+            self.assert_outputs(queue.handle_script_error, "handle_script_error", [tool, {"patch": self.mock_work_item}, ScriptError(message="ScriptError error message", script_args="MockErrorCommand", output="MOCK output")], expected_stdout, expected_stderr, expected_exceptions)
diff --git a/Tools/Scripts/webkitpy/tool/commands/rebaseline.py b/Tools/Scripts/webkitpy/tool/commands/rebaseline.py
new file mode 100644
index 0000000..d9209b1
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/rebaseline.py
@@ -0,0 +1,500 @@
+# 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 json
+import logging
+import optparse
+import sys
+
+from webkitpy.common.checkout.baselineoptimizer import BaselineOptimizer
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.layout_tests.controllers.test_result_writer import TestResultWriter
+from webkitpy.layout_tests.models import test_failures
+from webkitpy.layout_tests.models.test_expectations import TestExpectations, BASELINE_SUFFIX_LIST
+from webkitpy.layout_tests.port import builders
+from webkitpy.layout_tests.port import factory
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+
+
+_log = logging.getLogger(__name__)
+
+
+# FIXME: Should TestResultWriter know how to compute this string?
+def _baseline_name(fs, test_name, suffix):
+    return fs.splitext(test_name)[0] + TestResultWriter.FILENAME_SUFFIX_EXPECTED + "." + suffix
+
+
+class AbstractRebaseliningCommand(AbstractDeclarativeCommand):
+    # not overriding execute() - pylint: disable-msg=W0223
+
+    move_overwritten_baselines_option = optparse.make_option("--move-overwritten-baselines", action="store_true", default=False,
+        help="Move overwritten baselines elsewhere in the baseline path. This is for bringing up new ports.")
+
+    no_optimize_option = optparse.make_option('--no-optimize', dest='optimize', action='store_false', default=True,
+        help=('Do not optimize/de-dup the expectations after rebaselining (default is to de-dup automatically). '
+              'You can use "webkit-patch optimize-baselines" to optimize separately.'))
+
+    platform_options = factory.platform_options(use_globs=True)
+
+    results_directory_option = optparse.make_option("--results-directory", help="Local results directory to use")
+
+    suffixes_option = optparse.make_option("--suffixes", default=','.join(BASELINE_SUFFIX_LIST), action="store",
+        help="Comma-separated-list of file types to rebaseline")
+
+    def __init__(self, options=None):
+        super(AbstractRebaseliningCommand, self).__init__(options=options)
+        self._baseline_suffix_list = BASELINE_SUFFIX_LIST
+
+
+class RebaselineTest(AbstractRebaseliningCommand):
+    name = "rebaseline-test-internal"
+    help_text = "Rebaseline a single test from a buildbot. Only intended for use by other webkit-patch commands."
+
+    def __init__(self):
+        super(RebaselineTest, self).__init__(options=[
+            self.no_optimize_option,
+            self.results_directory_option,
+            self.suffixes_option,
+            optparse.make_option("--builder", help="Builder to pull new baselines from"),
+            optparse.make_option("--move-overwritten-baselines-to", action="append", default=[],
+                help="Platform to move existing baselines to before rebaselining. This is for bringing up new ports."),
+            optparse.make_option("--test", help="Test to rebaseline"),
+            ])
+        self._scm_changes = {'add': []}
+
+    def _results_url(self, builder_name):
+        return self._tool.buildbot_for_builder_name(builder_name).builder_with_name(builder_name).latest_layout_test_results_url()
+
+    def _baseline_directory(self, builder_name):
+        port = self._tool.port_factory.get_from_builder_name(builder_name)
+        override_dir = builders.rebaseline_override_dir(builder_name)
+        if override_dir:
+            return self._tool.filesystem.join(port.layout_tests_dir(), 'platform', override_dir)
+        return port.baseline_version_dir()
+
+    def _copy_existing_baseline(self, move_overwritten_baselines_to, test_name, suffix):
+        old_baselines = []
+        new_baselines = []
+
+        # Need to gather all the baseline paths before modifying the filesystem since
+        # the modifications can affect the results of port.expected_filename.
+        for platform in move_overwritten_baselines_to:
+            port = self._tool.port_factory.get(platform)
+            old_baseline = port.expected_filename(test_name, "." + suffix)
+            if not self._tool.filesystem.exists(old_baseline):
+                _log.debug("No existing baseline for %s." % test_name)
+                continue
+
+            new_baseline = self._tool.filesystem.join(port.baseline_path(), self._file_name_for_expected_result(test_name, suffix))
+            if self._tool.filesystem.exists(new_baseline):
+                _log.debug("Existing baseline at %s, not copying over it." % new_baseline)
+                continue
+
+            old_baselines.append(old_baseline)
+            new_baselines.append(new_baseline)
+
+        for i in range(len(old_baselines)):
+            old_baseline = old_baselines[i]
+            new_baseline = new_baselines[i]
+
+            _log.debug("Copying baseline from %s to %s." % (old_baseline, new_baseline))
+            self._tool.filesystem.maybe_make_directory(self._tool.filesystem.dirname(new_baseline))
+            self._tool.filesystem.copyfile(old_baseline, new_baseline)
+            if not self._tool.scm().exists(new_baseline):
+                self._add_to_scm(new_baseline)
+
+    def _save_baseline(self, data, target_baseline):
+        if not data:
+            return
+        filesystem = self._tool.filesystem
+        filesystem.maybe_make_directory(filesystem.dirname(target_baseline))
+        filesystem.write_binary_file(target_baseline, data)
+        if not self._tool.scm().exists(target_baseline):
+            self._add_to_scm(target_baseline)
+
+    def _add_to_scm(self, path):
+        self._scm_changes['add'].append(path)
+
+    def _update_expectations_file(self, builder_name, test_name):
+        port = self._tool.port_factory.get_from_builder_name(builder_name)
+
+        # Since rebaseline-test-internal can be called multiple times in parallel,
+        # we need to ensure that we're not trying to update the expectations file
+        # concurrently as well.
+        # FIXME: We should rework the code to not need this; maybe just download
+        # the files in parallel and rebaseline local files serially?
+        try:
+            path = port.path_to_test_expectations_file()
+            lock = self._tool.make_file_lock(path + '.lock')
+            lock.acquire_lock()
+            expectations = TestExpectations(port, include_overrides=False)
+            for test_configuration in port.all_test_configurations():
+                if test_configuration.version == port.test_configuration().version:
+                    expectationsString = expectations.remove_configuration_from_test(test_name, test_configuration)
+
+            self._tool.filesystem.write_text_file(path, expectationsString)
+        finally:
+            lock.release_lock()
+
+    def _test_root(self, test_name):
+        return self._tool.filesystem.splitext(test_name)[0]
+
+    def _file_name_for_actual_result(self, test_name, suffix):
+        return "%s-actual.%s" % (self._test_root(test_name), suffix)
+
+    def _file_name_for_expected_result(self, test_name, suffix):
+        return "%s-expected.%s" % (self._test_root(test_name), suffix)
+
+    def _rebaseline_test(self, builder_name, test_name, move_overwritten_baselines_to, suffix, results_url):
+        baseline_directory = self._baseline_directory(builder_name)
+
+        source_baseline = "%s/%s" % (results_url, self._file_name_for_actual_result(test_name, suffix))
+        target_baseline = self._tool.filesystem.join(baseline_directory, self._file_name_for_expected_result(test_name, suffix))
+
+        if move_overwritten_baselines_to:
+            self._copy_existing_baseline(move_overwritten_baselines_to, test_name, suffix)
+
+        _log.debug("Retrieving %s." % source_baseline)
+        self._save_baseline(self._tool.web.get_binary(source_baseline, convert_404_to_None=True), target_baseline)
+
+    def _rebaseline_test_and_update_expectations(self, options):
+        if options.results_directory:
+            results_url = 'file://' + options.results_directory
+        else:
+            results_url = self._results_url(options.builder)
+        self._baseline_suffix_list = options.suffixes.split(',')
+        for suffix in self._baseline_suffix_list:
+            self._rebaseline_test(options.builder, options.test, options.move_overwritten_baselines_to, suffix, results_url)
+        self._update_expectations_file(options.builder, options.test)
+
+    def execute(self, options, args, tool):
+        self._rebaseline_test_and_update_expectations(options)
+        print json.dumps(self._scm_changes)
+
+
+class OptimizeBaselines(AbstractRebaseliningCommand):
+    name = "optimize-baselines"
+    help_text = "Reshuffles the baselines for the given tests to use as litte space on disk as possible."
+    argument_names = "TEST_NAMES"
+
+    def __init__(self):
+        super(OptimizeBaselines, self).__init__(options=[self.suffixes_option] + self.platform_options)
+
+    def _optimize_baseline(self, optimizer, test_name):
+        for suffix in self._baseline_suffix_list:
+            baseline_name = _baseline_name(self._tool.filesystem, test_name, suffix)
+            if not optimizer.optimize(baseline_name):
+                print "Heuristics failed to optimize %s" % baseline_name
+
+    def execute(self, options, args, tool):
+        self._baseline_suffix_list = options.suffixes.split(',')
+        port_names = tool.port_factory.all_port_names(options.platform)
+        if not port_names:
+            print "No port names match '%s'" % options.platform
+            return
+
+        optimizer = BaselineOptimizer(tool, port_names)
+        port = tool.port_factory.get(port_names[0])
+        for test_name in port.tests(args):
+            _log.info("Optimizing %s" % test_name)
+            self._optimize_baseline(optimizer, test_name)
+
+
+class AnalyzeBaselines(AbstractRebaseliningCommand):
+    name = "analyze-baselines"
+    help_text = "Analyzes the baselines for the given tests and prints results that are identical."
+    argument_names = "TEST_NAMES"
+
+    def __init__(self):
+        super(AnalyzeBaselines, self).__init__(options=[
+            self.suffixes_option,
+            optparse.make_option('--missing', action='store_true', default=False, help='show missing baselines as well'),
+            ] + self.platform_options)
+        self._optimizer_class = BaselineOptimizer  # overridable for testing
+        self._baseline_optimizer = None
+        self._port = None
+
+    def _write(self, msg):
+        print msg
+
+    def _analyze_baseline(self, options, test_name):
+        for suffix in self._baseline_suffix_list:
+            baseline_name = _baseline_name(self._tool.filesystem, test_name, suffix)
+            results_by_directory = self._baseline_optimizer.read_results_by_directory(baseline_name)
+            if results_by_directory:
+                self._write("%s:" % baseline_name)
+                self._baseline_optimizer.write_by_directory(results_by_directory, self._write, "  ")
+            elif options.missing:
+                self._write("%s: (no baselines found)" % baseline_name)
+
+    def execute(self, options, args, tool):
+        self._baseline_suffix_list = options.suffixes.split(',')
+        port_names = tool.port_factory.all_port_names(options.platform)
+        if not port_names:
+            print "No port names match '%s'" % options.platform
+            return
+
+        self._baseline_optimizer = self._optimizer_class(tool, port_names)
+        self._port = tool.port_factory.get(port_names[0])
+        for test_name in self._port.tests(args):
+            self._analyze_baseline(options, test_name)
+
+
+class AbstractParallelRebaselineCommand(AbstractRebaseliningCommand):
+    # not overriding execute() - pylint: disable-msg=W0223
+
+    def _run_webkit_patch(self, args, verbose):
+        try:
+            verbose_args = ['--verbose'] if verbose else []
+            stderr = self._tool.executive.run_command([self._tool.path()] + verbose_args + args, cwd=self._tool.scm().checkout_root, return_stderr=True)
+            for line in stderr.splitlines():
+                print >> sys.stderr, line
+        except ScriptError, e:
+            _log.error(e)
+
+    def _builders_to_fetch_from(self, builders_to_check):
+        # This routine returns the subset of builders that will cover all of the baseline search paths
+        # used in the input list. In particular, if the input list contains both Release and Debug
+        # versions of a configuration, we *only* return the Release version (since we don't save
+        # debug versions of baselines).
+        release_builders = set()
+        debug_builders = set()
+        builders_to_fallback_paths = {}
+        for builder in builders_to_check:
+            port = self._tool.port_factory.get_from_builder_name(builder)
+            if port.test_configuration().build_type == 'Release':
+                release_builders.add(builder)
+            else:
+                debug_builders.add(builder)
+        for builder in list(release_builders) + list(debug_builders):
+            port = self._tool.port_factory.get_from_builder_name(builder)
+            fallback_path = port.baseline_search_path()
+            if fallback_path not in builders_to_fallback_paths.values():
+                builders_to_fallback_paths[builder] = fallback_path
+        return builders_to_fallback_paths.keys()
+
+    def _rebaseline_commands(self, test_list, options):
+
+        path_to_webkit_patch = self._tool.path()
+        cwd = self._tool.scm().checkout_root
+        commands = []
+        for test in test_list:
+            for builder in self._builders_to_fetch_from(test_list[test]):
+                suffixes = ','.join(test_list[test][builder])
+                cmd_line = [path_to_webkit_patch, 'rebaseline-test-internal', '--suffixes', suffixes, '--builder', builder, '--test', test]
+                if options.move_overwritten_baselines:
+                    move_overwritten_baselines_to = builders.move_overwritten_baselines_to(builder)
+                    for platform in move_overwritten_baselines_to:
+                        cmd_line.extend(['--move-overwritten-baselines-to', platform])
+                if options.results_directory:
+                    cmd_line.extend(['--results-directory', options.results_directory])
+                if options.verbose:
+                    cmd_line.append('--verbose')
+                commands.append(tuple([cmd_line, cwd]))
+        return commands
+
+    def _files_to_add(self, command_results):
+        files_to_add = set()
+        for output in [result[1].split('\n') for result in command_results]:
+            file_added = False
+            for line in output:
+                try:
+                    if line:
+                        files_to_add.update(json.loads(line)['add'])
+                        file_added = True
+                except ValueError:
+                    _log.debug('"%s" is not a JSON object, ignoring' % line)
+
+            if not file_added:
+                _log.debug('Could not add file based off output "%s"' % output)
+
+
+        return list(files_to_add)
+
+    def _optimize_baselines(self, test_list, verbose=False):
+        # We don't run this in parallel because modifying the SCM in parallel is unreliable.
+        for test in test_list:
+            all_suffixes = set()
+            for builder in self._builders_to_fetch_from(test_list[test]):
+                all_suffixes.update(test_list[test][builder])
+            # FIXME: We should propagate the platform options as well.
+            self._run_webkit_patch(['optimize-baselines', '--suffixes', ','.join(all_suffixes), test], verbose)
+
+    def _rebaseline(self, options, test_list):
+        for test, builders_to_check in sorted(test_list.items()):
+            _log.info("Rebaselining %s" % test)
+            for builder, suffixes in sorted(builders_to_check.items()):
+                _log.debug("  %s: %s" % (builder, ",".join(suffixes)))
+
+        commands = self._rebaseline_commands(test_list, options)
+        command_results = self._tool.executive.run_in_parallel(commands)
+
+        log_output = '\n'.join(result[2] for result in command_results).replace('\n\n', '\n')
+        for line in log_output.split('\n'):
+            if line:
+                print >> sys.stderr, line  # FIXME: Figure out how to log properly.
+
+        files_to_add = self._files_to_add(command_results)
+        if files_to_add:
+            self._tool.scm().add_list(list(files_to_add))
+
+        if options.optimize:
+            self._optimize_baselines(test_list, options.verbose)
+
+
+class RebaselineJson(AbstractParallelRebaselineCommand):
+    name = "rebaseline-json"
+    help_text = "Rebaseline based off JSON passed to stdin. Intended to only be called from other scripts."
+
+    def __init__(self,):
+        super(RebaselineJson, self).__init__(options=[
+            self.move_overwritten_baselines_option,
+            self.no_optimize_option,
+            self.results_directory_option,
+            ])
+
+    def execute(self, options, args, tool):
+        self._rebaseline(options, json.loads(sys.stdin.read()))
+
+
+class RebaselineExpectations(AbstractParallelRebaselineCommand):
+    name = "rebaseline-expectations"
+    help_text = "Rebaselines the tests indicated in TestExpectations."
+
+    def __init__(self):
+        super(RebaselineExpectations, self).__init__(options=[
+            self.move_overwritten_baselines_option,
+            self.no_optimize_option,
+            ] + self.platform_options)
+        self._test_list = None
+
+    def _update_expectations_files(self, port_name):
+        port = self._tool.port_factory.get(port_name)
+
+        expectations = TestExpectations(port)
+        for path in port.expectations_dict():
+            if self._tool.filesystem.exists(path):
+                self._tool.filesystem.write_text_file(path, expectations.remove_rebaselined_tests(expectations.get_rebaselining_failures(), path))
+
+    def _tests_to_rebaseline(self, port):
+        tests_to_rebaseline = {}
+        expectations = TestExpectations(port, include_overrides=True)
+        for test in expectations.get_rebaselining_failures():
+            tests_to_rebaseline[test] = TestExpectations.suffixes_for_expectations(expectations.get_expectations(test))
+        return tests_to_rebaseline
+
+    def _add_tests_to_rebaseline_for_port(self, port_name):
+        builder_name = builders.builder_name_for_port_name(port_name)
+        if not builder_name:
+            return
+        tests = self._tests_to_rebaseline(self._tool.port_factory.get(port_name)).items()
+
+        if tests:
+            _log.info("Retrieving results for %s from %s." % (port_name, builder_name))
+
+        for test_name, suffixes in tests:
+            _log.info("    %s (%s)" % (test_name, ','.join(suffixes)))
+            if test_name not in self._test_list:
+                self._test_list[test_name] = {}
+            self._test_list[test_name][builder_name] = suffixes
+
+    def execute(self, options, args, tool):
+        options.results_directory = None
+        self._test_list = {}
+        port_names = tool.port_factory.all_port_names(options.platform)
+        for port_name in port_names:
+            self._add_tests_to_rebaseline_for_port(port_name)
+        if not self._test_list:
+            _log.warning("Did not find any tests marked Rebaseline.")
+            return
+
+        self._rebaseline(options, self._test_list)
+
+        for port_name in port_names:
+            self._update_expectations_files(port_name)
+
+
+class Rebaseline(AbstractParallelRebaselineCommand):
+    name = "rebaseline"
+    help_text = "Rebaseline tests with results from the build bots. Shows the list of failing tests on the builders if no test names are provided."
+    argument_names = "[TEST_NAMES]"
+
+    def __init__(self):
+        super(Rebaseline, self).__init__(options=[
+            self.move_overwritten_baselines_option,
+            self.no_optimize_option,
+            # FIXME: should we support the platform options in addition to (or instead of) --builders?
+            self.suffixes_option,
+            optparse.make_option("--builders", default=None, action="append", help="Comma-separated-list of builders to pull new baselines from (can also be provided multiple times)"),
+            ])
+
+    def _builders_to_pull_from(self):
+        chromium_buildbot_builder_names = []
+        webkit_buildbot_builder_names = []
+        for name in builders.all_builder_names():
+            if self._tool.port_factory.get_from_builder_name(name).is_chromium():
+                chromium_buildbot_builder_names.append(name)
+            else:
+                webkit_buildbot_builder_names.append(name)
+
+        titles = ["build.webkit.org bots", "build.chromium.org bots"]
+        lists = [webkit_buildbot_builder_names, chromium_buildbot_builder_names]
+
+        chosen_names = self._tool.user.prompt_with_multiple_lists("Which builder to pull results from:", titles, lists, can_choose_multiple=True)
+        return [self._builder_with_name(name) for name in chosen_names]
+
+    def _builder_with_name(self, name):
+        return self._tool.buildbot_for_builder_name(name).builder_with_name(name)
+
+    def _tests_to_update(self, builder):
+        failing_tests = builder.latest_layout_test_results().tests_matching_failure_types([test_failures.FailureTextMismatch])
+        return self._tool.user.prompt_with_list("Which test(s) to rebaseline for %s:" % builder.name(), failing_tests, can_choose_multiple=True)
+
+    def execute(self, options, args, tool):
+        options.results_directory = None
+        if options.builders:
+            builders_to_check = []
+            for builder_names in options.builders:
+                builders_to_check += [self._builder_with_name(name) for name in builder_names.split(",")]
+        else:
+            builders_to_check = self._builders_to_pull_from()
+
+        test_list = {}
+        suffixes_to_update = options.suffixes.split(",")
+
+        for builder in builders_to_check:
+            tests = args or self._tests_to_update(builder)
+            for test in tests:
+                if test not in test_list:
+                    test_list[test] = {}
+                test_list[test][builder.name()] = suffixes_to_update
+
+        if options.verbose:
+            _log.debug("rebaseline-json: " + str(test_list))
+
+        self._rebaseline(options, test_list)
diff --git a/Tools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py b/Tools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py
new file mode 100644
index 0000000..d7dafb9
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py
@@ -0,0 +1,401 @@
+# 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.system.outputcapture import OutputCapture
+from webkitpy.common.checkout.baselineoptimizer import BaselineOptimizer
+from webkitpy.common.net.buildbot.buildbot_mock import MockBuilder
+from webkitpy.common.system.executive_mock import MockExecutive2
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.commands.rebaseline import *
+from webkitpy.tool.mocktool import MockTool, MockOptions
+
+
+class _BaseTestCase(unittest.TestCase):
+    MOCK_WEB_RESULT = 'MOCK Web result, convert 404 to None=True'
+    WEB_PREFIX = 'http://example.com/f/builders/WebKit Mac10.7/results/layout-test-results'
+
+    command_constructor = None
+
+    def setUp(self):
+        self.tool = MockTool()
+        self.command = self.command_constructor()  # lint warns that command_constructor might not be set, but this is intentional; pylint: disable-msg=E1102
+        self.command.bind_to_tool(self.tool)
+        self.lion_port = self.tool.port_factory.get_from_builder_name("WebKit Mac10.7")
+        self.lion_expectations_path = self.lion_port.path_to_test_expectations_file()
+
+        # FIXME: we should override builders._exact_matches here to point to a set
+        # of test ports and restore the value in tearDown(), and that way the
+        # individual tests wouldn't have to worry about it.
+
+    def _expand(self, path):
+        if self.tool.filesystem.isabs(path):
+            return path
+        return self.tool.filesystem.join(self.lion_port.layout_tests_dir(), path)
+
+    def _read(self, path):
+        return self.tool.filesystem.read_text_file(self._expand(path))
+
+    def _write(self, path, contents):
+        self.tool.filesystem.write_text_file(self._expand(path), contents)
+
+    def _zero_out_test_expectations(self):
+        for port_name in self.tool.port_factory.all_port_names():
+            port = self.tool.port_factory.get(port_name)
+            for path in port.expectations_files():
+                self._write(path, '')
+        self.tool.filesystem.written_files = {}
+
+
+class TestRebaselineTest(_BaseTestCase):
+    command_constructor = RebaselineTest  # AKA webkit-patch rebaseline-test-internal
+
+    def setUp(self):
+        super(TestRebaselineTest, self).setUp()
+        self.options = MockOptions(builder="WebKit Mac10.7", test="userscripts/another-test.html", suffixes="txt",
+                                   move_overwritten_baselines_to=None, results_directory=None)
+
+    def test_baseline_directory(self):
+        command = self.command
+        self.assertEqual(command._baseline_directory("Apple Win XP Debug (Tests)"), "/mock-checkout/LayoutTests/platform/win-xp")
+        self.assertEqual(command._baseline_directory("Apple Win 7 Release (Tests)"), "/mock-checkout/LayoutTests/platform/win")
+        self.assertEqual(command._baseline_directory("Apple Lion Release WK1 (Tests)"), "/mock-checkout/LayoutTests/platform/mac-lion")
+        self.assertEqual(command._baseline_directory("Apple Lion Release WK2 (Tests)"), "/mock-checkout/LayoutTests/platform/mac-wk2")
+        self.assertEqual(command._baseline_directory("GTK Linux 32-bit Release"), "/mock-checkout/LayoutTests/platform/gtk")
+        self.assertEqual(command._baseline_directory("EFL Linux 64-bit Debug"), "/mock-checkout/LayoutTests/platform/efl-wk1")
+        self.assertEqual(command._baseline_directory("Qt Linux Release"), "/mock-checkout/LayoutTests/platform/qt")
+        self.assertEqual(command._baseline_directory("WebKit Mac10.7"), "/mock-checkout/LayoutTests/platform/chromium-mac-lion")
+        self.assertEqual(command._baseline_directory("WebKit Mac10.6"), "/mock-checkout/LayoutTests/platform/chromium-mac-snowleopard")
+
+    def test_rebaseline_updates_expectations_file_noop(self):
+        self._zero_out_test_expectations()
+        self._write(self.lion_expectations_path, """Bug(B) [ Mac Linux XP Debug ] fast/dom/Window/window-postmessage-clone-really-deep-array.html [ Pass ]
+Bug(A) [ Debug ] : fast/css/large-list-of-rules-crash.html [ Failure ]
+""")
+        self._write("fast/dom/Window/window-postmessage-clone-really-deep-array.html", "Dummy test contents")
+        self._write("fast/css/large-list-of-rules-crash.html", "Dummy test contents")
+        self._write("userscripts/another-test.html", "Dummy test contents")
+
+        self.options.suffixes = "png,wav,txt"
+        self.command._rebaseline_test_and_update_expectations(self.options)
+
+        self.assertEquals(self.tool.web.urls_fetched,
+            [self.WEB_PREFIX + '/userscripts/another-test-actual.png',
+             self.WEB_PREFIX + '/userscripts/another-test-actual.wav',
+             self.WEB_PREFIX + '/userscripts/another-test-actual.txt'])
+        new_expectations = self._read(self.lion_expectations_path)
+        self.assertEqual(new_expectations, """Bug(B) [ Mac Linux XP Debug ] fast/dom/Window/window-postmessage-clone-really-deep-array.html [ Pass ]
+Bug(A) [ Debug ] : fast/css/large-list-of-rules-crash.html [ Failure ]
+""")
+
+    def test_rebaseline_updates_expectations_file(self):
+        self._write(self.lion_expectations_path, "Bug(x) [ Mac ] userscripts/another-test.html [ ImageOnlyFailure ]\nbug(z) [ Linux ] userscripts/another-test.html [ ImageOnlyFailure ]\n")
+        self._write("userscripts/another-test.html", "Dummy test contents")
+
+        self.options.suffixes = 'png,wav,txt'
+        self.command._rebaseline_test_and_update_expectations(self.options)
+
+        self.assertEquals(self.tool.web.urls_fetched,
+            [self.WEB_PREFIX + '/userscripts/another-test-actual.png',
+             self.WEB_PREFIX + '/userscripts/another-test-actual.wav',
+             self.WEB_PREFIX + '/userscripts/another-test-actual.txt'])
+        new_expectations = self._read(self.lion_expectations_path)
+        self.assertEqual(new_expectations, "Bug(x) [ MountainLion SnowLeopard ] userscripts/another-test.html [ ImageOnlyFailure ]\nbug(z) [ Linux ] userscripts/another-test.html [ ImageOnlyFailure ]\n")
+
+    def test_rebaseline_does_not_include_overrides(self):
+        self._write(self.lion_expectations_path, "Bug(x) [ Mac ] userscripts/another-test.html [ ImageOnlyFailure ]\nBug(z) [ Linux ] userscripts/another-test.html [ ImageOnlyFailure ]\n")
+        self._write(self.lion_port.path_from_chromium_base('skia', 'skia_test_expectations.txt'), "Bug(y) [ Mac ] other-test.html [ Failure ]\n")
+        self._write("userscripts/another-test.html", "Dummy test contents")
+
+        self.options.suffixes = 'png,wav,txt'
+        self.command._rebaseline_test_and_update_expectations(self.options)
+
+        self.assertEquals(self.tool.web.urls_fetched,
+            [self.WEB_PREFIX + '/userscripts/another-test-actual.png',
+             self.WEB_PREFIX + '/userscripts/another-test-actual.wav',
+             self.WEB_PREFIX + '/userscripts/another-test-actual.txt'])
+
+        new_expectations = self._read(self.lion_expectations_path)
+        self.assertEqual(new_expectations, "Bug(x) [ MountainLion SnowLeopard ] userscripts/another-test.html [ ImageOnlyFailure ]\nBug(z) [ Linux ] userscripts/another-test.html [ ImageOnlyFailure ]\n")
+
+    def test_rebaseline_test(self):
+        self.command._rebaseline_test("WebKit Linux", "userscripts/another-test.html", None, "txt", self.WEB_PREFIX)
+        self.assertEquals(self.tool.web.urls_fetched, [self.WEB_PREFIX + '/userscripts/another-test-actual.txt'])
+
+    def test_rebaseline_test_with_results_directory(self):
+        self._write(self.lion_expectations_path, "Bug(x) [ Mac ] userscripts/another-test.html [ ImageOnlyFailure ]\nbug(z) [ Linux ] userscripts/another-test.html [ ImageOnlyFailure ]\n")
+        self.options.results_directory = '/tmp'
+        self.command._rebaseline_test_and_update_expectations(self.options)
+        self.assertEquals(self.tool.web.urls_fetched, ['file:///tmp/userscripts/another-test-actual.txt'])
+
+    def test_rebaseline_test_and_print_scm_changes(self):
+        self.command._print_scm_changes = True
+        self.command._scm_changes = {'add': [], 'delete': []}
+        self.tool._scm.exists = lambda x: False
+
+        self.command._rebaseline_test("WebKit Linux", "userscripts/another-test.html", None, "txt", None)
+
+        self.assertEquals(self.command._scm_changes, {'add': ['/mock-checkout/LayoutTests/platform/chromium-linux/userscripts/another-test-expected.txt'], 'delete': []})
+
+    def test_rebaseline_and_copy_test(self):
+        self._write("userscripts/another-test-expected.txt", "generic result")
+
+        self.command._rebaseline_test("WebKit Mac10.7", "userscripts/another-test.html", ["chromium-mac-snowleopard"], "txt", None)
+
+        self.assertEquals(self._read('platform/chromium-mac-lion/userscripts/another-test-expected.txt'), self.MOCK_WEB_RESULT)
+        self.assertEquals(self._read('platform/chromium-mac-snowleopard/userscripts/another-test-expected.txt'), 'generic result')
+
+    def test_rebaseline_and_copy_test_no_existing_result(self):
+        self.command._rebaseline_test("WebKit Mac10.7", "userscripts/another-test.html", ["chromium-mac-snowleopard"], "txt", None)
+
+        self.assertEquals(self._read('platform/chromium-mac-lion/userscripts/another-test-expected.txt'), self.MOCK_WEB_RESULT)
+        self.assertFalse(self.tool.filesystem.exists(self._expand('platform/chromium-mac-snowleopard/userscripts/another-test-expected.txt')))
+
+    def test_rebaseline_and_copy_test_with_lion_result(self):
+        self._write("platform/chromium-mac-lion/userscripts/another-test-expected.txt", "original lion result")
+
+        self.command._rebaseline_test("WebKit Mac10.7", "userscripts/another-test.html", ["chromium-mac-snowleopard"], "txt", self.WEB_PREFIX)
+
+        self.assertEquals(self.tool.web.urls_fetched, [self.WEB_PREFIX + '/userscripts/another-test-actual.txt'])
+        self.assertEquals(self._read("platform/chromium-mac-snowleopard/userscripts/another-test-expected.txt"), "original lion result")
+        self.assertEquals(self._read("platform/chromium-mac-lion/userscripts/another-test-expected.txt"), self.MOCK_WEB_RESULT)
+
+    def test_rebaseline_and_copy_no_overwrite_test(self):
+        self._write("platform/chromium-mac-lion/userscripts/another-test-expected.txt", "original lion result")
+        self._write("platform/chromium-mac-snowleopard/userscripts/another-test-expected.txt", "original snowleopard result")
+
+        self.command._rebaseline_test("WebKit Mac10.7", "userscripts/another-test.html", ["chromium-mac-snowleopard"], "txt", None)
+
+        self.assertEquals(self._read("platform/chromium-mac-snowleopard/userscripts/another-test-expected.txt"), "original snowleopard result")
+        self.assertEquals(self._read("platform/chromium-mac-lion/userscripts/another-test-expected.txt"), self.MOCK_WEB_RESULT)
+
+    def test_rebaseline_test_internal_with_move_overwritten_baselines_to(self):
+        self.tool.executive = MockExecutive2()
+
+        # FIXME: it's confusing that this is the test- port, and not the regular lion port. Really all of the tests should be using the test ports.
+        port = self.tool.port_factory.get('test-mac-snowleopard')
+        self._write(port._filesystem.join(port.layout_tests_dir(), 'platform/test-mac-snowleopard/failures/expected/image-expected.txt'), 'original snowleopard result')
+
+        old_exact_matches = builders._exact_matches
+        oc = OutputCapture()
+        try:
+            builders._exact_matches = {
+                "MOCK Leopard": {"port_name": "test-mac-leopard", "specifiers": set(["mock-specifier"])},
+                "MOCK SnowLeopard": {"port_name": "test-mac-snowleopard", "specifiers": set(["mock-specifier"])},
+            }
+
+            options = MockOptions(optimize=True, builder="MOCK SnowLeopard", suffixes="txt",
+                move_overwritten_baselines_to=["test-mac-leopard"], verbose=True, test="failures/expected/image.html",
+                results_directory=None)
+
+            oc.capture_output()
+            self.command.execute(options, [], self.tool)
+        finally:
+            out, _, _ = oc.restore_output()
+            builders._exact_matches = old_exact_matches
+
+        self.assertEquals(self._read(self.tool.filesystem.join(port.layout_tests_dir(), 'platform/test-mac-leopard/failures/expected/image-expected.txt')), 'original snowleopard result')
+        self.assertEquals(out, '{"add": []}\n')
+
+
+class TestRebaselineJson(_BaseTestCase):
+    command_constructor = RebaselineJson
+
+    def setUp(self):
+        super(TestRebaselineJson, self).setUp()
+        self.tool.executive = MockExecutive2()
+        self.old_exact_matches = builders._exact_matches
+        builders._exact_matches = {
+            "MOCK builder": {"port_name": "test-mac-snowleopard", "specifiers": set(["mock-specifier"]),
+                             "move_overwritten_baselines_to": ["test-mac-leopard"]},
+            "MOCK builder (Debug)": {"port_name": "test-mac-snowleopard", "specifiers": set(["mock-specifier", "debug"])},
+        }
+
+    def tearDown(self):
+        builders._exact_matches = self.old_exact_matches
+        super(TestRebaselineJson, self).tearDown()
+
+    def test_rebaseline_all(self):
+        options = MockOptions(optimize=True, verbose=True, move_overwritten_baselines=False, results_directory=None)
+        self.command._rebaseline(options,  {"user-scripts/another-test.html": {"MOCK builder": ["txt", "png"]}})
+
+        # Note that we have one run_in_parallel() call followed by a run_command()
+        self.assertEquals(self.tool.executive.calls,
+            [[['echo', 'rebaseline-test-internal', '--suffixes', 'txt,png', '--builder', 'MOCK builder', '--test', 'user-scripts/another-test.html', '--verbose']],
+             ['echo', '--verbose', 'optimize-baselines', '--suffixes', 'txt,png', 'user-scripts/another-test.html']])
+
+    def test_rebaseline_debug(self):
+        options = MockOptions(optimize=True, verbose=True, move_overwritten_baselines=False, results_directory=None)
+        self.command._rebaseline(options,  {"user-scripts/another-test.html": {"MOCK builder (Debug)": ["txt", "png"]}})
+
+        # Note that we have one run_in_parallel() call followed by a run_command()
+        self.assertEquals(self.tool.executive.calls,
+            [[['echo', 'rebaseline-test-internal', '--suffixes', 'txt,png', '--builder', 'MOCK builder (Debug)', '--test', 'user-scripts/another-test.html', '--verbose']],
+             ['echo', '--verbose', 'optimize-baselines', '--suffixes', 'txt,png', 'user-scripts/another-test.html']])
+
+    def test_move_overwritten(self):
+        options = MockOptions(optimize=True, verbose=True, move_overwritten_baselines=True, results_directory=None)
+        self.command._rebaseline(options,  {"user-scripts/another-test.html": {"MOCK builder": ["txt", "png"]}})
+
+        # Note that we have one run_in_parallel() call followed by a run_command()
+        self.assertEquals(self.tool.executive.calls,
+            [[['echo', 'rebaseline-test-internal', '--suffixes', 'txt,png', '--builder', 'MOCK builder', '--test', 'user-scripts/another-test.html', '--move-overwritten-baselines-to', 'test-mac-leopard', '--verbose']],
+             ['echo', '--verbose', 'optimize-baselines', '--suffixes', 'txt,png', 'user-scripts/another-test.html']])
+
+    def test_no_optimize(self):
+        options = MockOptions(optimize=False, verbose=True, move_overwritten_baselines=False, results_directory=None)
+        self.command._rebaseline(options,  {"user-scripts/another-test.html": {"MOCK builder (Debug)": ["txt", "png"]}})
+
+        # Note that we have only one run_in_parallel() call
+        self.assertEquals(self.tool.executive.calls,
+            [[['echo', 'rebaseline-test-internal', '--suffixes', 'txt,png', '--builder', 'MOCK builder (Debug)', '--test', 'user-scripts/another-test.html', '--verbose']]])
+
+    def test_results_directory(self):
+        options = MockOptions(optimize=False, verbose=True, move_overwritten_baselines=False, results_directory='/tmp')
+        self.command._rebaseline(options,  {"user-scripts/another-test.html": {"MOCK builder": ["txt", "png"]}})
+
+        # Note that we have only one run_in_parallel() call
+        self.assertEquals(self.tool.executive.calls,
+            [[['echo', 'rebaseline-test-internal', '--suffixes', 'txt,png', '--builder', 'MOCK builder', '--test', 'user-scripts/another-test.html', '--results-directory', '/tmp', '--verbose']]])
+
+
+class TestRebaseline(_BaseTestCase):
+    # This command shares most of its logic with RebaselineJson, so these tests just test what is different.
+
+    command_constructor = Rebaseline  # AKA webkit-patch rebaseline
+
+    def test_tests_to_update(self):
+        build = Mock()
+        OutputCapture().assert_outputs(self, self.command._tests_to_update, [build])
+
+    def test_rebaseline(self):
+        self.command._builders_to_pull_from = lambda: [MockBuilder('MOCK builder')]
+        self.command._tests_to_update = lambda builder: ['mock/path/to/test.html']
+
+        self._zero_out_test_expectations()
+
+        old_exact_matches = builders._exact_matches
+        oc = OutputCapture()
+        try:
+            builders._exact_matches = {
+                "MOCK builder": {"port_name": "test-mac-leopard", "specifiers": set(["mock-specifier"])},
+            }
+            oc.capture_output()
+            self.command.execute(MockOptions(optimize=False, builders=None, suffixes="txt,png", verbose=True, move_overwritten_baselines=False), [], self.tool)
+        finally:
+            oc.restore_output()
+            builders._exact_matches = old_exact_matches
+
+        calls = filter(lambda x: x != ['qmake', '-v'] and x[0] != 'perl', self.tool.executive.calls)
+        self.assertEquals(calls,
+            [[['echo', 'rebaseline-test-internal', '--suffixes', 'txt,png', '--builder', 'MOCK builder', '--test', 'mock/path/to/test.html', '--verbose']]])
+
+
+class TestRebaselineExpectations(_BaseTestCase):
+    command_constructor = RebaselineExpectations
+
+    def setUp(self):
+        super(TestRebaselineExpectations, self).setUp()
+        self.options = MockOptions(optimize=False, builders=None, suffixes=['txt'], verbose=False, platform=None,
+                                   move_overwritten_baselines=False, results_directory=None)
+
+    def test_rebaseline_expectations(self):
+        self._zero_out_test_expectations()
+
+        self.tool.executive = MockExecutive2()
+
+        self.command._tests_to_rebaseline = lambda port: {'userscripts/another-test.html': set(['txt']), 'userscripts/images.svg': set(['png'])}
+        self.command.execute(self.options, [], self.tool)
+
+        # FIXME: change this to use the test- ports.
+        calls = filter(lambda x: x != ['qmake', '-v'], self.tool.executive.calls)
+        self.assertTrue(len(calls) == 1)
+        self.assertTrue(len(calls[0]) == 26)
+
+    def test_rebaseline_expectations_noop(self):
+        self._zero_out_test_expectations()
+
+        oc = OutputCapture()
+        try:
+            oc.capture_output()
+            self.command.execute(self.options, [], self.tool)
+        finally:
+            _, _, logs = oc.restore_output()
+            self.assertEquals(self.tool.filesystem.written_files, {})
+            self.assertEquals(logs, 'Did not find any tests marked Rebaseline.\n')
+
+    def disabled_test_overrides_are_included_correctly(self):
+        # This tests that the any tests marked as REBASELINE in the overrides are found, but
+        # that the overrides do not get written into the main file.
+        self._zero_out_test_expectations()
+
+        self._write(self.lion_expectations_path, '')
+        self.lion_port.expectations_dict = lambda: {
+            self.lion_expectations_path: '',
+            'overrides': ('Bug(x) userscripts/another-test.html [ Failure Rebaseline ]\n'
+                          'Bug(y) userscripts/test.html [ Crash ]\n')}
+        self._write('/userscripts/another-test.html', '')
+
+        self.assertEquals(self.command._tests_to_rebaseline(self.lion_port), {'userscripts/another-test.html': set(['png', 'txt', 'wav'])})
+        self.assertEquals(self._read(self.lion_expectations_path), '')
+
+
+class _FakeOptimizer(BaselineOptimizer):
+    def read_results_by_directory(self, baseline_name):
+        if baseline_name.endswith('txt'):
+            return {'LayoutTests/passes/text.html': '123456',
+                    'LayoutTests/platform/test-mac-leopard/passes/text.html': 'abcdef'}
+        return {}
+
+
+class TestAnalyzeBaselines(_BaseTestCase):
+    command_constructor = AnalyzeBaselines
+
+    def setUp(self):
+        super(TestAnalyzeBaselines, self).setUp()
+        self.port = self.tool.port_factory.get('test')
+        self.tool.port_factory.get = (lambda port_name=None, options=None: self.port)
+        self.lines = []
+        self.command._optimizer_class = _FakeOptimizer
+        self.command._write = (lambda msg: self.lines.append(msg))  # pylint bug warning about unnecessary lambda? pylint: disable-msg=W0108
+
+    def test_default(self):
+        self.command.execute(MockOptions(suffixes='txt', missing=False, platform=None), ['passes/text.html'], self.tool)
+        self.assertEquals(self.lines,
+            ['passes/text-expected.txt:',
+             '  (generic): 123456',
+             '  test-mac-leopard: abcdef'])
+
+    def test_missing_baselines(self):
+        self.command.execute(MockOptions(suffixes='png,txt', missing=True, platform=None), ['passes/text.html'], self.tool)
+        self.assertEquals(self.lines,
+            ['passes/text-expected.png: (no baselines found)',
+             'passes/text-expected.txt:',
+             '  (generic): 123456',
+             '  test-mac-leopard: abcdef'])
diff --git a/Tools/Scripts/webkitpy/tool/commands/rebaselineserver.py b/Tools/Scripts/webkitpy/tool/commands/rebaselineserver.py
new file mode 100644
index 0000000..09c6d0b
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/rebaselineserver.py
@@ -0,0 +1,98 @@
+# 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.
+
+"""Starts a local HTTP server which displays layout test failures (given a test
+results directory), provides comparisons of expected and actual results (both
+images and text) and allows one-click rebaselining of tests."""
+
+from webkitpy.common import system
+from webkitpy.common.net.resultsjsonparser import for_each_test, JSONTestResult
+from webkitpy.layout_tests.layout_package import json_results_generator
+from webkitpy.tool.commands.abstractlocalservercommand import AbstractLocalServerCommand
+from webkitpy.tool.servers.rebaselineserver import get_test_baselines, RebaselineHTTPServer, STATE_NEEDS_REBASELINE
+
+
+class TestConfig(object):
+    def __init__(self, test_port, layout_tests_directory, results_directory, platforms, filesystem, scm):
+        self.test_port = test_port
+        self.layout_tests_directory = layout_tests_directory
+        self.results_directory = results_directory
+        self.platforms = platforms
+        self.filesystem = filesystem
+        self.scm = scm
+
+
+class RebaselineServer(AbstractLocalServerCommand):
+    name = "rebaseline-server"
+    help_text = __doc__
+    argument_names = "/path/to/results/directory"
+
+    server = RebaselineHTTPServer
+
+    def _gather_baselines(self, results_json):
+        # Rebaseline server and it's associated JavaScript expected the tests subtree to
+        # be key-value pairs instead of hierarchical.
+        # FIXME: make the rebaseline server use the hierarchical tree.
+        new_tests_subtree = {}
+
+        def gather_baselines_for_test(test_name, result_dict):
+            result = JSONTestResult(test_name, result_dict)
+            if result.did_pass_or_run_as_expected():
+                return
+            result_dict['state'] = STATE_NEEDS_REBASELINE
+            result_dict['baselines'] = get_test_baselines(test_name, self._test_config)
+            new_tests_subtree[test_name] = result_dict
+
+        for_each_test(results_json['tests'], gather_baselines_for_test)
+        results_json['tests'] = new_tests_subtree
+
+    def _prepare_config(self, options, args, tool):
+        results_directory = args[0]
+        filesystem = system.filesystem.FileSystem()
+        scm = self._tool.scm()
+
+        print 'Parsing full_results.json...'
+        results_json_path = filesystem.join(results_directory, 'full_results.json')
+        results_json = json_results_generator.load_json(filesystem, results_json_path)
+
+        port = tool.port_factory.get()
+        layout_tests_directory = port.layout_tests_dir()
+        platforms = filesystem.listdir(filesystem.join(layout_tests_directory, 'platform'))
+        self._test_config = TestConfig(port, layout_tests_directory, results_directory, platforms, filesystem, scm)
+
+        print 'Gathering current baselines...'
+        self._gather_baselines(results_json)
+
+        return {
+            'test_config': self._test_config,
+            "results_json": results_json,
+            "platforms_json": {
+                'platforms': platforms,
+                'defaultPlatform': port.name(),
+            },
+        }
diff --git a/Tools/Scripts/webkitpy/tool/commands/roll.py b/Tools/Scripts/webkitpy/tool/commands/roll.py
new file mode 100644
index 0000000..37481b2
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/roll.py
@@ -0,0 +1,74 @@
+# Copyright (c) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand
+
+from webkitpy.tool import steps
+
+
+class RollChromiumDEPS(AbstractSequencedCommand):
+    name = "roll-chromium-deps"
+    help_text = "Updates Chromium DEPS (defaults to the last-known good revision of Chromium)"
+    argument_names = "[CHROMIUM_REVISION]"
+    steps = [
+        steps.UpdateChromiumDEPS,
+        steps.PrepareChangeLogForDEPSRoll,
+        steps.ConfirmDiff,
+        steps.Commit,
+    ]
+
+    def _prepare_state(self, options, args, tool):
+        return {
+            "chromium_revision": (args and args[0]),
+        }
+
+
+class PostChromiumDEPSRoll(AbstractSequencedCommand):
+    name = "post-chromium-deps-roll"
+    help_text = "Posts a patch to update Chromium DEPS (revision defaults to the last-known good revision of Chromium)"
+    argument_names = "CHROMIUM_REVISION CHROMIUM_REVISION_NAME"
+    steps = [
+        steps.CleanWorkingDirectory,
+        steps.Update,
+        steps.UpdateChromiumDEPS,
+        steps.PrepareChangeLogForDEPSRoll,
+        steps.CreateBug,
+        steps.PostDiff,
+    ]
+
+    def _prepare_state(self, options, args, tool):
+        options.review = False
+        options.request_commit = True
+
+        chromium_revision = args[0]
+        chromium_revision_name = args[1]
+        return {
+            "chromium_revision": chromium_revision,
+            "bug_title": "Roll Chromium DEPS to %s" % chromium_revision_name,
+            "bug_description": "A DEPS roll a day keeps the build break away.",
+        }
diff --git a/Tools/Scripts/webkitpy/tool/commands/roll_unittest.py b/Tools/Scripts/webkitpy/tool/commands/roll_unittest.py
new file mode 100644
index 0000000..800bc5b
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/roll_unittest.py
@@ -0,0 +1,63 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.commands.roll import *
+from webkitpy.tool.mocktool import MockOptions, MockTool
+
+
+class RollCommandsTest(CommandsTest):
+    def test_update_chromium_deps(self):
+        expected_stderr = """Updating Chromium DEPS to 6764
+MOCK: MockDEPS.write_variable(chromium_rev, 6764)
+MOCK: user.open_url: file://...
+Was that diff correct?
+Committed r49824: <http://trac.webkit.org/changeset/49824>
+"""
+        self.assert_execute_outputs(RollChromiumDEPS(), [6764], expected_stderr=expected_stderr)
+
+    def test_update_chromium_deps_older_revision(self):
+        options = MockOptions(non_interactive=False)
+        expected_stderr = """Current Chromium DEPS revision 6564 is newer than 5764.
+ERROR: Unable to update Chromium DEPS
+"""
+        self.assert_execute_outputs(RollChromiumDEPS(), [5764], options=options, expected_stderr=expected_stderr, expected_exception=SystemExit)
+
+
+class PostRollCommandsTest(CommandsTest):
+    def test_prepare_state(self):
+        postroll = PostChromiumDEPSRoll()
+        options = MockOptions()
+        tool = MockTool()
+        lkgr_state = postroll._prepare_state(options, [None, "last-known good revision"], tool)
+        self.assertEquals(None, lkgr_state["chromium_revision"])
+        self.assertEquals("Roll Chromium DEPS to last-known good revision", lkgr_state["bug_title"])
+        revision_state = postroll._prepare_state(options, ["1234", "r1234"], tool)
+        self.assertEquals("1234", revision_state["chromium_revision"])
+        self.assertEquals("Roll Chromium DEPS to r1234", revision_state["bug_title"])
diff --git a/Tools/Scripts/webkitpy/tool/commands/sheriffbot.py b/Tools/Scripts/webkitpy/tool/commands/sheriffbot.py
new file mode 100644
index 0000000..d30da39
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/sheriffbot.py
@@ -0,0 +1,74 @@
+# Copyright (c) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.tool.bot.sheriff import Sheriff
+from webkitpy.tool.bot.irc_command import commands as irc_commands
+from webkitpy.tool.bot.ircbot import IRCBot
+from webkitpy.tool.commands.queues import AbstractQueue
+from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler
+
+
+class SheriffBot(AbstractQueue, StepSequenceErrorHandler):
+    name = "sheriff-bot"
+    watchers = AbstractQueue.watchers + [
+        "abarth@webkit.org",
+        "eric@webkit.org",
+    ]
+
+    # AbstractQueue methods
+
+    def begin_work_queue(self):
+        AbstractQueue.begin_work_queue(self)
+        self._sheriff = Sheriff(self._tool, self)
+        self._irc_bot = IRCBot("sheriffbot", self._tool, self._sheriff, irc_commands)
+        self._tool.ensure_irc_connected(self._irc_bot.irc_delegate())
+
+    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()
+        return
+
+    def process_work_item(self, failure_map):
+        return True
+
+    def handle_unexpected_error(self, failure_map, message):
+        log(message)
+
+    # StepSequenceErrorHandler methods
+
+    @classmethod
+    def handle_script_error(cls, tool, state, script_error):
+        # Ideally we would post some information to IRC about what went wrong
+        # here, but we don't have the IRC password in the child process.
+        pass
diff --git a/Tools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py b/Tools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py
new file mode 100644
index 0000000..9aa57b1
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py
@@ -0,0 +1,33 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.commands.queuestest import QueuesTest
+
+
+class SheriffBotTest(QueuesTest):
+    pass  # No unittests as the moment.
diff --git a/Tools/Scripts/webkitpy/tool/commands/stepsequence.py b/Tools/Scripts/webkitpy/tool/commands/stepsequence.py
new file mode 100644
index 0000000..b666554
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/stepsequence.py
@@ -0,0 +1,83 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool import steps
+
+from webkitpy.common.checkout.scm import CheckoutNeedsUpdate
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.tool.bot.queueengine import QueueEngine
+
+
+class StepSequenceErrorHandler():
+    @classmethod
+    def handle_script_error(cls, tool, patch, script_error):
+        raise NotImplementedError, "subclasses must implement"
+
+    @classmethod
+    def handle_checkout_needs_update(cls, tool, state, options, error):
+        raise NotImplementedError, "subclasses must implement"
+
+
+class StepSequence(object):
+    def __init__(self, steps):
+        self._steps = steps or []
+
+    def options(self):
+        collected_options = [
+            steps.Options.parent_command,
+            steps.Options.quiet,
+        ]
+        for step in self._steps:
+            collected_options = collected_options + step.options()
+        # Remove duplicates.
+        collected_options = sorted(set(collected_options))
+        return collected_options
+
+    def _run(self, tool, options, state):
+        for step in self._steps:
+            step(tool, options).run(state)
+
+    def run_and_handle_errors(self, tool, options, state=None):
+        if not state:
+            state = {}
+        try:
+            self._run(tool, options, state)
+        except CheckoutNeedsUpdate, e:
+            log("Commit failed because the checkout is out of date.  Please update and try again.")
+            if options.parent_command:
+                command = tool.command_by_name(options.parent_command)
+                command.handle_checkout_needs_update(tool, state, options, e)
+            QueueEngine.exit_after_handled_error(e)
+        except ScriptError, e:
+            if not options.quiet:
+                log(e.message_with_output())
+            if options.parent_command:
+                command = tool.command_by_name(options.parent_command)
+                command.handle_script_error(tool, state, e)
+            QueueEngine.exit_after_handled_error(e)
diff --git a/Tools/Scripts/webkitpy/tool/commands/suggestnominations.py b/Tools/Scripts/webkitpy/tool/commands/suggestnominations.py
new file mode 100644
index 0000000..c197a11
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/suggestnominations.py
@@ -0,0 +1,247 @@
+# Copyright (c) 2011 Google Inc. All rights reserved.
+# Copyright (c) 2011 Code Aurora Forum. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from optparse import make_option
+import re
+
+from webkitpy.common.checkout.changelog import ChangeLogEntry
+from webkitpy.common.config.committers import CommitterList
+from webkitpy.tool import steps
+from webkitpy.tool.grammar import join_with_separators
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+
+
+class SuggestNominations(AbstractDeclarativeCommand):
+    name = "suggest-nominations"
+    help_text = "Suggest contributors for committer/reviewer nominations"
+
+    def __init__(self):
+        options = [
+            make_option("--committer-minimum", action="store", dest="committer_minimum", type="int", default=10, help="Specify minimum patch count for Committer nominations."),
+            make_option("--reviewer-minimum", action="store", dest="reviewer_minimum", type="int", default=80, help="Specify minimum patch count for Reviewer nominations."),
+            make_option("--max-commit-age", action="store", dest="max_commit_age", type="int", default=9, help="Specify max commit age to consider for nominations (in months)."),
+            make_option("--show-commits", action="store_true", dest="show_commits", default=False, help="Show commit history with nomination suggestions."),
+        ]
+
+        AbstractDeclarativeCommand.__init__(self, options=options)
+        # FIXME: This should probably be on the tool somewhere.
+        self._committer_list = CommitterList()
+
+    _counters_by_name = {}
+    _counters_by_email = {}
+
+    def _init_options(self, options):
+        self.committer_minimum = options.committer_minimum
+        self.reviewer_minimum = options.reviewer_minimum
+        self.max_commit_age = options.max_commit_age
+        self.show_commits = options.show_commits
+        self.verbose = options.verbose
+
+    # FIXME: This should move to scm.py
+    def _recent_commit_messages(self):
+        git_log = self._tool.executive.run_command(['git', 'log', '--since="%s months ago"' % self.max_commit_age])
+        match_git_svn_id = re.compile(r"\n\n    git-svn-id:.*\n", re.MULTILINE)
+        match_get_log_lines = re.compile(r"^\S.*\n", re.MULTILINE)
+        match_leading_indent = re.compile(r"^[ ]{4}", re.MULTILINE)
+
+        messages = re.split(r"commit \w{40}", git_log)[1:]  # Ignore the first message which will be empty.
+        for message in messages:
+            # Remove any lines from git and unindent all the lines
+            (message, _) = match_git_svn_id.subn("", message)
+            (message, _) = match_get_log_lines.subn("", message)
+            (message, _) = match_leading_indent.subn("", message)
+            yield message.lstrip()  # Remove any leading newlines from the log message.
+
+    # e.g. Patch by Eric Seidel <eric@webkit.org> on 2011-09-15
+    patch_by_regexp = r'^Patch by (?P<name>.+?)\s+<(?P<email>[^<>]+)> on (?P<date>\d{4}-\d{2}-\d{2})$'
+
+    def _count_recent_patches(self):
+        # This entire block could be written as a map/reduce over the messages.
+        for message in self._recent_commit_messages():
+            # FIXME: This should use ChangeLogEntry to do the entire parse instead
+            # of grabbing at its regexps.
+            dateline_match = re.match(ChangeLogEntry.date_line_regexp, message, re.MULTILINE)
+            if not dateline_match:
+                # Modern commit messages don't just dump the ChangeLog entry, but rather
+                # have a special Patch by line for non-committers.
+                dateline_match = re.search(self.patch_by_regexp, message, re.MULTILINE)
+                if not dateline_match:
+                    continue
+
+            author_email = dateline_match.group("email")
+            if not author_email:
+                continue
+
+            # We only care about reviewed patches, so make sure it has a valid reviewer line.
+            reviewer_match = re.search(ChangeLogEntry.reviewed_by_regexp, message, re.MULTILINE)
+            # We might also want to validate the reviewer name against the committer list.
+            if not reviewer_match or not reviewer_match.group("reviewer"):
+                continue
+
+            author_name = dateline_match.group("name")
+            if not author_name:
+                continue
+
+            if re.search("([^a-zA-Z]and[^a-zA-Z])|(,)|(@)", author_name):
+                # This entry seems to have multiple reviewers, or invalid characters, so reject it.
+                continue
+
+            svn_id_match = re.search(ChangeLogEntry.svn_id_regexp, message, re.MULTILINE)
+            if svn_id_match:
+                svn_id = svn_id_match.group("svnid")
+            if not svn_id_match or not svn_id:
+                svn_id = "unknown"
+            commit_date = dateline_match.group("date")
+
+            # See if we already have a contributor with this name or email
+            counter_by_name = self._counters_by_name.get(author_name)
+            counter_by_email = self._counters_by_email.get(author_email)
+            if counter_by_name:
+                if counter_by_email:
+                    if counter_by_name != counter_by_email:
+                        # Merge these two counters  This is for the case where we had
+                        # John Smith (jsmith@gmail.com) and Jonathan Smith (jsmith@apple.com)
+                        # and just found a John Smith (jsmith@apple.com).  Now we know the
+                        # two names are the same person
+                        counter_by_name['names'] |= counter_by_email['names']
+                        counter_by_name['emails'] |= counter_by_email['emails']
+                        counter_by_name['count'] += counter_by_email.get('count', 0)
+                        self._counters_by_email[author_email] = counter_by_name
+                else:
+                    # Add email to the existing counter
+                    self._counters_by_email[author_email] = counter_by_name
+                    counter_by_name['emails'] |= set([author_email])
+            else:
+                if counter_by_email:
+                    # Add name to the existing counter
+                    self._counters_by_name[author_name] = counter_by_email
+                    counter_by_email['names'] |= set([author_name])
+                else:
+                    # Create new counter
+                    new_counter = {'names': set([author_name]), 'emails': set([author_email]), 'latest_name': author_name, 'latest_email': author_email, 'commits': ""}
+                    self._counters_by_name[author_name] = new_counter
+                    self._counters_by_email[author_email] = new_counter
+
+            assert(self._counters_by_name[author_name] == self._counters_by_email[author_email])
+            counter = self._counters_by_name[author_name]
+            counter['count'] = counter.get('count', 0) + 1
+
+            if svn_id.isdigit():
+                svn_id = "http://trac.webkit.org/changeset/" + svn_id
+            counter['commits'] += "  commit: %s on %s by %s (%s)\n" % (svn_id, commit_date, author_name, author_email)
+
+        return self._counters_by_email
+
+    def _collect_nominations(self, counters_by_email):
+        nominations = []
+        for author_email, counter in counters_by_email.items():
+            if author_email != counter['latest_email']:
+                continue
+            roles = []
+
+            contributor = self._committer_list.contributor_by_email(author_email)
+
+            author_name = counter['latest_name']
+            patch_count = counter['count']
+
+            if patch_count >= self.committer_minimum and (not contributor or not contributor.can_commit):
+                roles.append("committer")
+            if patch_count >= self.reviewer_minimum  and (not contributor or not contributor.can_review):
+                roles.append("reviewer")
+            if roles:
+                nominations.append({
+                    'roles': roles,
+                    'author_name': author_name,
+                    'author_email': author_email,
+                    'patch_count': patch_count,
+                })
+        return nominations
+
+    def _print_nominations(self, nominations):
+        def nomination_cmp(a_nomination, b_nomination):
+            roles_result = cmp(a_nomination['roles'], b_nomination['roles'])
+            if roles_result:
+                return -roles_result
+            count_result = cmp(a_nomination['patch_count'], b_nomination['patch_count'])
+            if count_result:
+                return -count_result
+            return cmp(a_nomination['author_name'], b_nomination['author_name'])
+
+        for nomination in sorted(nominations, nomination_cmp):
+            # This is a little bit of a hack, but its convienent to just pass the nomination dictionary to the formating operator.
+            nomination['roles_string'] = join_with_separators(nomination['roles']).upper()
+            print "%(roles_string)s: %(author_name)s (%(author_email)s) has %(patch_count)s reviewed patches" % nomination
+            counter = self._counters_by_email[nomination['author_email']]
+
+            if self.show_commits:
+                print counter['commits']
+
+    def _print_counts(self, counters_by_email):
+        def counter_cmp(a_tuple, b_tuple):
+            # split the tuples
+            # the second element is the "counter" structure
+            _, a_counter = a_tuple
+            _, b_counter = b_tuple
+
+            count_result = cmp(a_counter['count'], b_counter['count'])
+            if count_result:
+                return -count_result
+            return cmp(a_counter['latest_name'].lower(), b_counter['latest_name'].lower())
+
+        for author_email, counter in sorted(counters_by_email.items(), counter_cmp):
+            if author_email != counter['latest_email']:
+                continue
+            contributor = self._committer_list.contributor_by_email(author_email)
+            author_name = counter['latest_name']
+            patch_count = counter['count']
+            counter['names'] = counter['names'] - set([author_name])
+            counter['emails'] = counter['emails'] - set([author_email])
+
+            alias_list = []
+            for alias in counter['names']:
+                alias_list.append(alias)
+            for alias in counter['emails']:
+                alias_list.append(alias)
+            if alias_list:
+                print "CONTRIBUTOR: %s (%s) has %d reviewed patches %s" % (author_name, author_email, patch_count, "(aliases: " + ", ".join(alias_list) + ")")
+            else:
+                print "CONTRIBUTOR: %s (%s) has %d reviewed patches" % (author_name, author_email, patch_count)
+        return
+
+    def execute(self, options, args, tool):
+        self._init_options(options)
+        patch_counts = self._count_recent_patches()
+        nominations = self._collect_nominations(patch_counts)
+        self._print_nominations(nominations)
+        if self.verbose:
+            self._print_counts(patch_counts)
+
+
+if __name__ == "__main__":
+    SuggestNominations()
diff --git a/Tools/Scripts/webkitpy/tool/commands/suggestnominations_unittest.py b/Tools/Scripts/webkitpy/tool/commands/suggestnominations_unittest.py
new file mode 100644
index 0000000..88be253
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/suggestnominations_unittest.py
@@ -0,0 +1,77 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+# Copyright (C) 2011 Code Aurora Forum. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.commands.suggestnominations import SuggestNominations
+from webkitpy.tool.mocktool import MockOptions, MockTool
+
+
+class SuggestNominationsTest(CommandsTest):
+
+    mock_git_output = """commit 60831dde5beb22f35aef305a87fca7b5f284c698
+Author: fpizlo@apple.com <fpizlo@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
+Date:   Thu Sep 15 19:56:21 2011 +0000
+
+    Value profiles collect no information for global variables
+    https://bugs.webkit.org/show_bug.cgi?id=68143
+
+    Reviewed by Geoffrey Garen.
+
+    git-svn-id: http://svn.webkit.org/repository/webkit/trunk@95219 268f45cc-cd09-0410-ab3c-d52691b4dbfc
+"""
+    mock_same_author_commit_message = """Value profiles collect no information for global variables
+https://bugs.webkit.org/show_bug.cgi?id=68143
+
+Reviewed by Geoffrey Garen."""
+
+    def test_recent_commit_messages(self):
+        tool = MockTool()
+        suggest_nominations = SuggestNominations()
+        suggest_nominations._init_options(options=MockOptions(reviewer_minimum=80, committer_minimum=10, max_commit_age=9, show_commits=False, verbose=False))
+        suggest_nominations.bind_to_tool(tool)
+
+        tool.executive.run_command = lambda command: self.mock_git_output
+        self.assertEqual(list(suggest_nominations._recent_commit_messages()), [self.mock_same_author_commit_message])
+
+    mock_non_committer_commit_message = """Let TestWebKitAPI work for chromium
+https://bugs.webkit.org/show_bug.cgi?id=67756
+
+Patch by Xianzhu Wang <wangxianzhu@chromium.org> on 2011-09-15
+Reviewed by Sam Weinig.
+
+Source/WebKit/chromium:
+
+* WebKit.gyp:"""
+
+    def test_basic(self):
+        expected_stdout = "REVIEWER: Xianzhu Wang (wangxianzhu@chromium.org) has 88 reviewed patches\n"
+        suggest_nominations = SuggestNominations()
+        suggest_nominations._init_options(options=MockOptions(reviewer_minimum=80, committer_minimum=10, max_commit_age=9, show_commits=False, verbose=False))
+        suggest_nominations._recent_commit_messages = lambda: [self.mock_non_committer_commit_message for _ in range(88)]
+        self.assert_execute_outputs(suggest_nominations, [], expected_stdout=expected_stdout, options=MockOptions(reviewer_minimum=80, committer_minimum=10, max_commit_age=9, show_commits=False, verbose=False))
diff --git a/Tools/Scripts/webkitpy/tool/commands/upload.py b/Tools/Scripts/webkitpy/tool/commands/upload.py
new file mode 100644
index 0000000..6b52e6c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/upload.py
@@ -0,0 +1,505 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, 2010 Google Inc. All rights reserved.
+# Copyright (c) 2009 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:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import re
+import sys
+
+from optparse import make_option
+
+from webkitpy.tool import steps
+
+from webkitpy.common.checkout.changelog import parse_bug_id_from_changelog
+from webkitpy.common.config.committers import CommitterList
+from webkitpy.common.system.deprecated_logging import error, log
+from webkitpy.common.system.user import User
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand
+from webkitpy.tool.comments import bug_comment_from_svn_revision
+from webkitpy.tool.grammar import pluralize, join_with_separators
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+
+
+class CommitMessageForCurrentDiff(AbstractDeclarativeCommand):
+    name = "commit-message"
+    help_text = "Print a commit message suitable for the uncommitted changes"
+
+    def __init__(self):
+        options = [
+            steps.Options.git_commit,
+        ]
+        AbstractDeclarativeCommand.__init__(self, options=options)
+
+    def execute(self, options, args, tool):
+        # This command is a useful test to make sure commit_message_for_this_commit
+        # always returns the right value regardless of the current working directory.
+        print "%s" % tool.checkout().commit_message_for_this_commit(options.git_commit).message()
+
+
+class CleanPendingCommit(AbstractDeclarativeCommand):
+    name = "clean-pending-commit"
+    help_text = "Clear r+ on obsolete patches so they do not appear in the pending-commit list."
+
+    # NOTE: This was designed to be generic, but right now we're only processing patches from the pending-commit list, so only r+ matters.
+    def _flags_to_clear_on_patch(self, patch):
+        if not patch.is_obsolete():
+            return None
+        what_was_cleared = []
+        if patch.review() == "+":
+            if patch.reviewer():
+                what_was_cleared.append(u"%s's review+" % patch.reviewer().full_name)
+            else:
+                what_was_cleared.append("review+")
+        return join_with_separators(what_was_cleared)
+
+    def execute(self, options, args, tool):
+        committers = CommitterList()
+        for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list():
+            bug = self._tool.bugs.fetch_bug(bug_id)
+            patches = bug.patches(include_obsolete=True)
+            for patch in patches:
+                flags_to_clear = self._flags_to_clear_on_patch(patch)
+                if not flags_to_clear:
+                    continue
+                message = u"Cleared %s from obsolete attachment %s so that this bug does not appear in http://webkit.org/pending-commit." % (flags_to_clear, patch.id())
+                self._tool.bugs.obsolete_attachment(patch.id(), message)
+
+
+# FIXME: This should be share more logic with AssignToCommitter and CleanPendingCommit
+class CleanReviewQueue(AbstractDeclarativeCommand):
+    name = "clean-review-queue"
+    help_text = "Clear r? on obsolete patches so they do not appear in the pending-review list."
+
+    def execute(self, options, args, tool):
+        queue_url = "http://webkit.org/pending-review"
+        # We do this inefficient dance to be more like webkit.org/pending-review
+        # bugs.queries.fetch_bug_ids_from_review_queue() doesn't return
+        # closed bugs, but folks using /pending-review will see them. :(
+        for patch_id in tool.bugs.queries.fetch_attachment_ids_from_review_queue():
+            patch = self._tool.bugs.fetch_attachment(patch_id)
+            if not patch.review() == "?":
+                continue
+            attachment_obsolete_modifier = ""
+            if patch.is_obsolete():
+                attachment_obsolete_modifier = "obsolete "
+            elif patch.bug().is_closed():
+                bug_closed_explanation = "  If you would like this patch reviewed, please attach it to a new bug (or re-open this bug before marking it for review again)."
+            else:
+                # Neither the patch was obsolete or the bug was closed, next patch...
+                continue
+            message = "Cleared review? from %sattachment %s so that this bug does not appear in %s.%s" % (attachment_obsolete_modifier, patch.id(), queue_url, bug_closed_explanation)
+            self._tool.bugs.obsolete_attachment(patch.id(), message)
+
+
+class AssignToCommitter(AbstractDeclarativeCommand):
+    name = "assign-to-committer"
+    help_text = "Assign bug to whoever attached the most recent r+'d patch"
+
+    def _patches_have_commiters(self, reviewed_patches):
+        for patch in reviewed_patches:
+            if not patch.committer():
+                return False
+        return True
+
+    def _assign_bug_to_last_patch_attacher(self, bug_id):
+        committers = CommitterList()
+        bug = self._tool.bugs.fetch_bug(bug_id)
+        if not bug.is_unassigned():
+            assigned_to_email = bug.assigned_to_email()
+            log(u"Bug %s is already assigned to %s (%s)." % (bug_id, assigned_to_email, committers.committer_by_email(assigned_to_email)))
+            return
+
+        reviewed_patches = bug.reviewed_patches()
+        if not reviewed_patches:
+            log("Bug %s has no non-obsolete patches, ignoring." % bug_id)
+            return
+
+        # We only need to do anything with this bug if one of the r+'d patches does not have a valid committer (cq+ set).
+        if self._patches_have_commiters(reviewed_patches):
+            log("All reviewed patches on bug %s already have commit-queue+, ignoring." % bug_id)
+            return
+
+        latest_patch = reviewed_patches[-1]
+        attacher_email = latest_patch.attacher_email()
+        committer = committers.committer_by_email(attacher_email)
+        if not committer:
+            log("Attacher %s is not a committer.  Bug %s likely needs commit-queue+." % (attacher_email, bug_id))
+            return
+
+        reassign_message = u"Attachment %s was posted by a committer and has review+, assigning to %s for commit." % (latest_patch.id(), committer.full_name)
+        self._tool.bugs.reassign_bug(bug_id, committer.bugzilla_email(), reassign_message)
+
+    def execute(self, options, args, tool):
+        for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list():
+            self._assign_bug_to_last_patch_attacher(bug_id)
+
+
+class ObsoleteAttachments(AbstractSequencedCommand):
+    name = "obsolete-attachments"
+    help_text = "Mark all attachments on a bug as obsolete"
+    argument_names = "BUGID"
+    steps = [
+        steps.ObsoletePatches,
+    ]
+
+    def _prepare_state(self, options, args, tool):
+        return { "bug_id" : args[0] }
+
+
+class AttachToBug(AbstractSequencedCommand):
+    name = "attach-to-bug"
+    help_text = "Attach the the file to the bug"
+    argument_names = "BUGID FILEPATH"
+    steps = [
+        steps.AttachToBug,
+    ]
+
+    def _prepare_state(self, options, args, tool):
+        state = {}
+        state["bug_id"] = args[0]
+        state["filepath"] = args[1]
+        return state
+
+
+class AbstractPatchUploadingCommand(AbstractSequencedCommand):
+    def _bug_id(self, options, args, tool, state):
+        # Perfer a bug id passed as an argument over a bug url in the diff (i.e. ChangeLogs).
+        bug_id = args and args[0]
+        if not bug_id:
+            changed_files = self._tool.scm().changed_files(options.git_commit)
+            state["changed_files"] = changed_files
+            bug_id = tool.checkout().bug_id_for_this_commit(options.git_commit, changed_files)
+        return bug_id
+
+    def _prepare_state(self, options, args, tool):
+        state = {}
+        state["bug_id"] = self._bug_id(options, args, tool, state)
+        if not state["bug_id"]:
+            error("No bug id passed and no bug url found in ChangeLogs.")
+        return state
+
+
+class Post(AbstractPatchUploadingCommand):
+    name = "post"
+    help_text = "Attach the current working directory diff to a bug as a patch file"
+    argument_names = "[BUGID]"
+    steps = [
+        steps.ValidateChangeLogs,
+        steps.CheckStyle,
+        steps.ConfirmDiff,
+        steps.ObsoletePatches,
+        steps.SuggestReviewers,
+        steps.EnsureBugIsOpenAndAssigned,
+        steps.PostDiff,
+    ]
+
+
+class LandSafely(AbstractPatchUploadingCommand):
+    name = "land-safely"
+    help_text = "Land the current diff via the commit-queue"
+    argument_names = "[BUGID]"
+    long_help = """land-safely updates the ChangeLog with the reviewer listed
+    in bugs.webkit.org for BUGID (or the bug ID detected from the ChangeLog).
+    The command then uploads the current diff to the bug and marks it for
+    commit by the commit-queue."""
+    show_in_main_help = True
+    steps = [
+        steps.UpdateChangeLogsWithReviewer,
+        steps.ValidateChangeLogs,
+        steps.ObsoletePatches,
+        steps.EnsureBugIsOpenAndAssigned,
+        steps.PostDiffForCommit,
+    ]
+
+
+class Prepare(AbstractSequencedCommand):
+    name = "prepare"
+    help_text = "Creates a bug (or prompts for an existing bug) and prepares the ChangeLogs"
+    argument_names = "[BUGID]"
+    steps = [
+        steps.PromptForBugOrTitle,
+        steps.CreateBug,
+        steps.PrepareChangeLog,
+    ]
+
+    def _prepare_state(self, options, args, tool):
+        bug_id = args and args[0]
+        return { "bug_id" : bug_id }
+
+
+class Upload(AbstractPatchUploadingCommand):
+    name = "upload"
+    help_text = "Automates the process of uploading a patch for review"
+    argument_names = "[BUGID]"
+    show_in_main_help = True
+    steps = [
+        steps.ValidateChangeLogs,
+        steps.CheckStyle,
+        steps.PromptForBugOrTitle,
+        steps.CreateBug,
+        steps.PrepareChangeLog,
+        steps.EditChangeLog,
+        steps.ConfirmDiff,
+        steps.ObsoletePatches,
+        steps.SuggestReviewers,
+        steps.EnsureBugIsOpenAndAssigned,
+        steps.PostDiff,
+    ]
+    long_help = """upload uploads the current diff to bugs.webkit.org.
+    If no bug id is provided, upload will create a bug.
+    If the current diff does not have a ChangeLog, upload
+    will prepare a ChangeLog.  Once a patch is read, upload
+    will open the ChangeLogs for editing using the command in the
+    EDITOR environment variable and will display the diff using the
+    command in the PAGER environment variable."""
+
+    def _prepare_state(self, options, args, tool):
+        state = {}
+        state["bug_id"] = self._bug_id(options, args, tool, state)
+        return state
+
+
+class EditChangeLogs(AbstractSequencedCommand):
+    name = "edit-changelogs"
+    help_text = "Opens modified ChangeLogs in $EDITOR"
+    show_in_main_help = True
+    steps = [
+        steps.EditChangeLog,
+    ]
+
+
+class PostCommits(AbstractDeclarativeCommand):
+    name = "post-commits"
+    help_text = "Attach a range of local commits to bugs as patch files"
+    argument_names = "COMMITISH"
+
+    def __init__(self):
+        options = [
+            make_option("-b", "--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."),
+            make_option("--add-log-as-comment", action="store_true", dest="add_log_as_comment", default=False, help="Add commit log message as a comment when uploading the patch."),
+            make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: description from commit message)"),
+            steps.Options.obsolete_patches,
+            steps.Options.review,
+            steps.Options.request_commit,
+        ]
+        AbstractDeclarativeCommand.__init__(self, options=options, requires_local_commits=True)
+
+    def _comment_text_for_commit(self, options, commit_message, tool, commit_id):
+        comment_text = None
+        if (options.add_log_as_comment):
+            comment_text = commit_message.body(lstrip=True)
+            comment_text += "---\n"
+            comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
+        return comment_text
+
+    def execute(self, options, args, tool):
+        commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
+        if len(commit_ids) > 10: # We could lower this limit, 10 is too many for one bug as-is.
+            error("webkit-patch does not support attaching %s at once.  Are you sure you passed the right commit range?" % (pluralize("patch", len(commit_ids))))
+
+        have_obsoleted_patches = set()
+        for commit_id in commit_ids:
+            commit_message = tool.scm().commit_message_for_local_commit(commit_id)
+
+            # Prefer --bug-id=, then a bug url in the commit message, then a bug url in the entire commit diff (i.e. ChangeLogs).
+            bug_id = options.bug_id or parse_bug_id_from_changelog(commit_message.message()) or parse_bug_id_from_changelog(tool.scm().create_patch(git_commit=commit_id))
+            if not bug_id:
+                log("Skipping %s: No bug id found in commit or specified with --bug-id." % commit_id)
+                continue
+
+            if options.obsolete_patches and bug_id not in have_obsoleted_patches:
+                state = { "bug_id": bug_id }
+                steps.ObsoletePatches(tool, options).run(state)
+                have_obsoleted_patches.add(bug_id)
+
+            diff = tool.scm().create_patch(git_commit=commit_id)
+            description = options.description or commit_message.description(lstrip=True, strip_url=True)
+            comment_text = self._comment_text_for_commit(options, commit_message, tool, commit_id)
+            tool.bugs.add_patch_to_bug(bug_id, diff, description, comment_text, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
+
+
+# FIXME: This command needs to be brought into the modern age with steps and CommitInfo.
+class MarkBugFixed(AbstractDeclarativeCommand):
+    name = "mark-bug-fixed"
+    help_text = "Mark the specified bug as fixed"
+    argument_names = "[SVN_REVISION]"
+    def __init__(self):
+        options = [
+            make_option("--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."),
+            make_option("--comment", action="store", type="string", dest="comment", help="Text to include in bug comment."),
+            make_option("--open", action="store_true", default=False, dest="open_bug", help="Open bug in default web browser (Mac only)."),
+            make_option("--update-only", action="store_true", default=False, dest="update_only", help="Add comment to the bug, but do not close it."),
+        ]
+        AbstractDeclarativeCommand.__init__(self, options=options)
+
+    # FIXME: We should be using checkout().changelog_entries_for_revision(...) instead here.
+    def _fetch_commit_log(self, tool, svn_revision):
+        if not svn_revision:
+            return tool.scm().last_svn_commit_log()
+        return tool.scm().svn_commit_log(svn_revision)
+
+    def _determine_bug_id_and_svn_revision(self, tool, bug_id, svn_revision):
+        commit_log = self._fetch_commit_log(tool, svn_revision)
+
+        if not bug_id:
+            bug_id = parse_bug_id_from_changelog(commit_log)
+
+        if not svn_revision:
+            match = re.search("^r(?P<svn_revision>\d+) \|", commit_log, re.MULTILINE)
+            if match:
+                svn_revision = match.group('svn_revision')
+
+        if not bug_id or not svn_revision:
+            not_found = []
+            if not bug_id:
+                not_found.append("bug id")
+            if not svn_revision:
+                not_found.append("svn revision")
+            error("Could not find %s on command-line or in %s."
+                  % (" or ".join(not_found), "r%s" % svn_revision if svn_revision else "last commit"))
+
+        return (bug_id, svn_revision)
+
+    def execute(self, options, args, tool):
+        bug_id = options.bug_id
+
+        svn_revision = args and args[0]
+        if svn_revision:
+            if re.match("^r[0-9]+$", svn_revision, re.IGNORECASE):
+                svn_revision = svn_revision[1:]
+            if not re.match("^[0-9]+$", svn_revision):
+                error("Invalid svn revision: '%s'" % svn_revision)
+
+        needs_prompt = False
+        if not bug_id or not svn_revision:
+            needs_prompt = True
+            (bug_id, svn_revision) = self._determine_bug_id_and_svn_revision(tool, bug_id, svn_revision)
+
+        log("Bug: <%s> %s" % (tool.bugs.bug_url_for_bug_id(bug_id), tool.bugs.fetch_bug_dictionary(bug_id)["title"]))
+        log("Revision: %s" % svn_revision)
+
+        if options.open_bug:
+            tool.user.open_url(tool.bugs.bug_url_for_bug_id(bug_id))
+
+        if needs_prompt:
+            if not tool.user.confirm("Is this correct?"):
+                self._exit(1)
+
+        bug_comment = bug_comment_from_svn_revision(svn_revision)
+        if options.comment:
+            bug_comment = "%s\n\n%s" % (options.comment, bug_comment)
+
+        if options.update_only:
+            log("Adding comment to Bug %s." % bug_id)
+            tool.bugs.post_comment_to_bug(bug_id, bug_comment)
+        else:
+            log("Adding comment to Bug %s and marking as Resolved/Fixed." % bug_id)
+            tool.bugs.close_bug_as_fixed(bug_id, bug_comment)
+
+
+# FIXME: Requires unit test.  Blocking issue: too complex for now.
+class CreateBug(AbstractDeclarativeCommand):
+    name = "create-bug"
+    help_text = "Create a bug from local changes or local commits"
+    argument_names = "[COMMITISH]"
+
+    def __init__(self):
+        options = [
+            steps.Options.cc,
+            steps.Options.component,
+            make_option("--no-prompt", action="store_false", dest="prompt", default=True, help="Do not prompt for bug title and comment; use commit log instead."),
+            make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review."),
+            make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review."),
+        ]
+        AbstractDeclarativeCommand.__init__(self, options=options)
+
+    def create_bug_from_commit(self, options, args, tool):
+        commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
+        if len(commit_ids) > 3:
+            error("Are you sure you want to create one bug with %s patches?" % len(commit_ids))
+
+        commit_id = commit_ids[0]
+
+        bug_title = ""
+        comment_text = ""
+        if options.prompt:
+            (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
+        else:
+            commit_message = tool.scm().commit_message_for_local_commit(commit_id)
+            bug_title = commit_message.description(lstrip=True, strip_url=True)
+            comment_text = commit_message.body(lstrip=True)
+            comment_text += "---\n"
+            comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
+
+        diff = tool.scm().create_patch(git_commit=commit_id)
+        bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
+
+        if bug_id and len(commit_ids) > 1:
+            options.bug_id = bug_id
+            options.obsolete_patches = False
+            # FIXME: We should pass through --no-comment switch as well.
+            PostCommits.execute(self, options, commit_ids[1:], tool)
+
+    def create_bug_from_patch(self, options, args, tool):
+        bug_title = ""
+        comment_text = ""
+        if options.prompt:
+            (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
+        else:
+            commit_message = tool.checkout().commit_message_for_this_commit(options.git_commit)
+            bug_title = commit_message.description(lstrip=True, strip_url=True)
+            comment_text = commit_message.body(lstrip=True)
+
+        diff = tool.scm().create_patch(options.git_commit)
+        bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
+
+    def prompt_for_bug_title_and_comment(self):
+        bug_title = User.prompt("Bug title: ")
+        # FIXME: User should provide a function for doing this multi-line prompt.
+        print "Bug comment (hit ^D on blank line to end):"
+        lines = sys.stdin.readlines()
+        try:
+            sys.stdin.seek(0, os.SEEK_END)
+        except IOError:
+            # Cygwin raises an Illegal Seek (errno 29) exception when the above
+            # seek() call is made. Ignoring it seems to cause no harm.
+            # FIXME: Figure out a way to get avoid the exception in the first
+            # place.
+            pass
+        comment_text = "".join(lines)
+        return (bug_title, comment_text)
+
+    def execute(self, options, args, tool):
+        if len(args):
+            if (not tool.scm().supports_local_commits()):
+                error("Extra arguments not supported; patch is taken from working directory.")
+            self.create_bug_from_commit(options, args, tool)
+        else:
+            self.create_bug_from_patch(options, args, tool)
diff --git a/Tools/Scripts/webkitpy/tool/commands/upload_unittest.py b/Tools/Scripts/webkitpy/tool/commands/upload_unittest.py
new file mode 100644
index 0000000..185bb97
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/upload_unittest.py
@@ -0,0 +1,149 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.commands.upload import *
+from webkitpy.tool.mocktool import MockOptions, MockTool
+
+class UploadCommandsTest(CommandsTest):
+    def test_commit_message_for_current_diff(self):
+        tool = MockTool()
+        expected_stdout = "This is a fake commit message that is at least 50 characters.\n"
+        self.assert_execute_outputs(CommitMessageForCurrentDiff(), [], expected_stdout=expected_stdout, tool=tool)
+
+    def test_clean_pending_commit(self):
+        self.assert_execute_outputs(CleanPendingCommit(), [])
+
+    def test_assign_to_committer(self):
+        tool = MockTool()
+        expected_stderr = """Warning, attachment 10001 on bug 50000 has invalid committer (non-committer@example.com)
+MOCK reassign_bug: bug_id=50000, assignee=eric@webkit.org
+-- Begin comment --
+Attachment 10001 was posted by a committer and has review+, assigning to Eric Seidel for commit.
+-- End comment --
+Bug 50003 is already assigned to foo@foo.com (None).
+Bug 50002 has no non-obsolete patches, ignoring.
+"""
+        self.assert_execute_outputs(AssignToCommitter(), [], expected_stderr=expected_stderr, tool=tool)
+
+    def test_obsolete_attachments(self):
+        expected_stderr = "Obsoleting 2 old patches on bug 50000\n"
+        self.assert_execute_outputs(ObsoleteAttachments(), [50000], expected_stderr=expected_stderr)
+
+    def test_post(self):
+        options = MockOptions()
+        options.cc = None
+        options.check_style = True
+        options.check_style_filter = None
+        options.comment = None
+        options.description = "MOCK description"
+        options.request_commit = False
+        options.review = True
+        options.suggest_reviewers = False
+        expected_stderr = """MOCK: user.open_url: file://...
+Was that diff correct?
+Obsoleting 2 old patches on bug 50000
+MOCK reassign_bug: bug_id=50000, assignee=None
+MOCK add_patch_to_bug: bug_id=50000, description=MOCK description, mark_for_review=True, mark_for_commit_queue=False, mark_for_landing=False
+MOCK: user.open_url: http://example.com/50000
+"""
+        self.assert_execute_outputs(Post(), [50000], options=options, expected_stderr=expected_stderr)
+
+    def test_attach_to_bug(self):
+        options = MockOptions()
+        options.comment = "extra comment"
+        options.description = "file description"
+        expected_stderr = """MOCK add_attachment_to_bug: bug_id=50000, description=file description filename=None mimetype=None
+-- Begin comment --
+extra comment
+-- End comment --
+"""
+        self.assert_execute_outputs(AttachToBug(), [50000, "path/to/file.txt", "file description"], options=options, expected_stderr=expected_stderr)
+
+    def test_attach_to_bug_no_description_or_comment(self):
+        options = MockOptions()
+        options.comment = None
+        options.description = None
+        expected_stderr = """MOCK add_attachment_to_bug: bug_id=50000, description=file.txt filename=None mimetype=None
+"""
+        self.assert_execute_outputs(AttachToBug(), [50000, "path/to/file.txt"], options=options, expected_stderr=expected_stderr)
+
+    def test_land_safely(self):
+        expected_stderr = "Obsoleting 2 old patches on bug 50000\nMOCK reassign_bug: bug_id=50000, assignee=None\nMOCK add_patch_to_bug: bug_id=50000, description=Patch for landing, mark_for_review=False, mark_for_commit_queue=False, mark_for_landing=True\n"
+        self.assert_execute_outputs(LandSafely(), [50000], expected_stderr=expected_stderr)
+
+    def test_prepare_diff_with_arg(self):
+        self.assert_execute_outputs(Prepare(), [50000])
+
+    def test_prepare(self):
+        expected_stderr = "MOCK create_bug\nbug_title: Mock user response\nbug_description: Mock user response\ncomponent: MOCK component\ncc: MOCK cc\n"
+        self.assert_execute_outputs(Prepare(), [], expected_stderr=expected_stderr)
+
+    def test_upload(self):
+        options = MockOptions()
+        options.cc = None
+        options.check_style = True
+        options.check_style_filter = None
+        options.comment = None
+        options.description = "MOCK description"
+        options.request_commit = False
+        options.review = True
+        options.suggest_reviewers = False
+        expected_stderr = """MOCK: user.open_url: file://...
+Was that diff correct?
+Obsoleting 2 old patches on bug 50000
+MOCK reassign_bug: bug_id=50000, assignee=None
+MOCK add_patch_to_bug: bug_id=50000, description=MOCK description, mark_for_review=True, mark_for_commit_queue=False, mark_for_landing=False
+MOCK: user.open_url: http://example.com/50000
+"""
+        self.assert_execute_outputs(Upload(), [50000], options=options, expected_stderr=expected_stderr)
+
+    def test_mark_bug_fixed(self):
+        tool = MockTool()
+        tool._scm.last_svn_commit_log = lambda: "r9876 |"
+        options = Mock()
+        options.bug_id = 50000
+        options.comment = "MOCK comment"
+        expected_stderr = """Bug: <http://example.com/50000> Bug with two r+'d and cq+'d patches, one of which has an invalid commit-queue setter.
+Revision: 9876
+MOCK: user.open_url: http://example.com/50000
+Is this correct?
+Adding comment to Bug 50000.
+MOCK bug comment: bug_id=50000, cc=None
+--- Begin comment ---
+MOCK comment
+
+Committed r9876: <http://trac.webkit.org/changeset/9876>
+--- End comment ---
+
+"""
+        self.assert_execute_outputs(MarkBugFixed(), [], expected_stderr=expected_stderr, tool=tool, options=options)
+
+    def test_edit_changelog(self):
+        self.assert_execute_outputs(EditChangeLogs(), [])
diff --git a/Tools/Scripts/webkitpy/tool/comments.py b/Tools/Scripts/webkitpy/tool/comments.py
new file mode 100755
index 0000000..771953e
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/comments.py
@@ -0,0 +1,42 @@
+# Copyright (c) 2009 Google Inc. All rights reserved.
+# Copyright (c) 2009 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:
+#
+#     * 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 tool for automating dealing with bugzilla, posting patches, committing
+# patches, etc.
+
+from webkitpy.common.config import urls
+
+
+def bug_comment_from_svn_revision(svn_revision):
+    return "Committed r%s: <%s>" % (svn_revision, urls.view_revision_url(svn_revision))
+
+
+def bug_comment_from_commit_text(scm, commit_text):
+    svn_revision = scm.svn_revision_from_commit_text(commit_text)
+    return bug_comment_from_svn_revision(svn_revision)
diff --git a/Tools/Scripts/webkitpy/tool/grammar.py b/Tools/Scripts/webkitpy/tool/grammar.py
new file mode 100644
index 0000000..8db9826
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/grammar.py
@@ -0,0 +1,54 @@
+# Copyright (c) 2009 Google Inc. All rights reserved.
+# Copyright (c) 2009 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:
+#
+#     * 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 re
+
+
+def plural(noun):
+    # This is a dumb plural() implementation that is just enough for our uses.
+    if re.search("h$", noun):
+        return noun + "es"
+    else:
+        return noun + "s"
+
+
+def pluralize(noun, count):
+    if count != 1:
+        noun = plural(noun)
+    return "%d %s" % (count, noun)
+
+
+def join_with_separators(list_of_strings, separator=', ', only_two_separator=" and ", last_separator=', and '):
+    if not list_of_strings:
+        return ""
+    if len(list_of_strings) == 1:
+        return list_of_strings[0]
+    if len(list_of_strings) == 2:
+        return only_two_separator.join(list_of_strings)
+    return "%s%s%s" % (separator.join(list_of_strings[:-1]), last_separator, list_of_strings[-1])
diff --git a/Tools/Scripts/webkitpy/tool/grammar_unittest.py b/Tools/Scripts/webkitpy/tool/grammar_unittest.py
new file mode 100644
index 0000000..cab71db
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/grammar_unittest.py
@@ -0,0 +1,41 @@
+# 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.tool.grammar import join_with_separators
+
+class GrammarTest(unittest.TestCase):
+
+    def test_join_with_separators(self):
+        self.assertEqual(join_with_separators(["one"]), "one")
+        self.assertEqual(join_with_separators(["one", "two"]), "one and two")
+        self.assertEqual(join_with_separators(["one", "two", "three"]), "one, two, and three")
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/tool/main.py b/Tools/Scripts/webkitpy/tool/main.py
new file mode 100755
index 0000000..68348a0
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/main.py
@@ -0,0 +1,108 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+# Copyright (c) 2009 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:
+#
+#     * 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 tool for automating dealing with bugzilla, posting patches, committing patches, etc.
+
+from optparse import make_option
+import os
+import threading
+
+from webkitpy.common.config.ports import DeprecatedPort
+from webkitpy.common.host import Host
+from webkitpy.common.net.irc import ircproxy
+from webkitpy.common.net.statusserver import StatusServer
+from webkitpy.tool.multicommandtool import MultiCommandTool
+from webkitpy.tool import commands
+
+
+class WebKitPatch(MultiCommandTool, Host):
+    global_options = [
+        make_option("-v", "--verbose", action="store_true", dest="verbose", default=False, help="enable all logging"),
+        make_option("-d", "--directory", action="append", dest="patch_directories", default=[], help="Directory to look at for changed files"),
+        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):
+        MultiCommandTool.__init__(self)
+        Host.__init__(self)
+        self._path = path
+        self.status_server = StatusServer()
+
+        self.wakeup_event = threading.Event()
+        self._irc = None
+        self._deprecated_port = None
+
+    # FIXME: Rename this deprecated_port()
+    def port(self):
+        return self._deprecated_port
+
+    def path(self):
+        return self._path
+
+    def ensure_irc_connected(self, irc_delegate):
+        if not self._irc:
+            self._irc = ircproxy.IRCProxy(irc_delegate)
+
+    def irc(self):
+        # We don't automatically construct IRCProxy here because constructing
+        # IRCProxy actually connects to IRC.  We want clients to explicitly
+        # connect to IRC.
+        return self._irc
+
+    def command_completed(self):
+        if self._irc:
+            self._irc.disconnect()
+
+    def should_show_in_main_help(self, command):
+        if not command.show_in_main_help:
+            return False
+        if command.requires_local_commits:
+            return self.scm().supports_local_commits()
+        return True
+
+    # FIXME: This may be unnecessary since we pass global options to all commands during execute() as well.
+    def handle_global_options(self, options):
+        self.initialize_scm(options.patch_directories)
+        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._deprecated_port = DeprecatedPort.port(options.port)
+
+    def should_execute_command(self, command):
+        if command.requires_local_commits and not self.scm().supports_local_commits():
+            failure_reason = "%s requires local commits using %s in %s." % (command.name, self.scm().display_name(), self.scm().checkout_root)
+            return (False, failure_reason)
+        return (True, None)
diff --git a/Tools/Scripts/webkitpy/tool/mocktool.py b/Tools/Scripts/webkitpy/tool/mocktool.py
new file mode 100644
index 0000000..b8f0976
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/mocktool.py
@@ -0,0 +1,88 @@
+# Copyright (C) 2011 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 threading
+
+from webkitpy.common.host_mock import MockHost
+from webkitpy.common.net.buildbot.buildbot_mock import MockBuildBot
+from webkitpy.common.net.statusserver_mock import MockStatusServer
+from webkitpy.common.net.irc.irc_mock import MockIRC
+
+# FIXME: Old-style "Ports" need to die and be replaced by modern layout_tests.port which needs to move to common.
+from webkitpy.common.config.ports_mock import MockPort
+
+
+# FIXME: We should just replace this with optparse.Values(default=kwargs)
+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.
+        self.update(**kwargs)
+
+    def update(self, **kwargs):
+        self.__dict__.update(**kwargs)
+        return self
+
+    def ensure_value(self, key, value):
+        if getattr(self, key, None) == None:
+            self.__dict__[key] = value
+        return self.__dict__[key]
+
+
+# FIXME: This should be renamed MockWebKitPatch.
+class MockTool(MockHost):
+    def __init__(self, *args, **kwargs):
+        MockHost.__init__(self, *args, **kwargs)
+
+        self._deprecated_port = MockPort()
+        self.status_server = MockStatusServer()
+
+        self._irc = None
+        self.irc_password = "MOCK irc password"
+        self.wakeup_event = threading.Event()
+
+    def port(self):
+        return self._deprecated_port
+
+    def path(self):
+        return "echo"
+
+    def ensure_irc_connected(self, delegate):
+        if not self._irc:
+            self._irc = MockIRC()
+
+    def irc(self):
+        return self._irc
+
+    def buildbot_for_builder_name(self, name):
+        return MockBuildBot()
diff --git a/Tools/Scripts/webkitpy/tool/mocktool_unittest.py b/Tools/Scripts/webkitpy/tool/mocktool_unittest.py
new file mode 100644
index 0000000..cceaa2e
--- /dev/null
+++ b/Tools/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/Tools/Scripts/webkitpy/tool/multicommandtool.py b/Tools/Scripts/webkitpy/tool/multicommandtool.py
new file mode 100644
index 0000000..38c410c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/multicommandtool.py
@@ -0,0 +1,317 @@
+# Copyright (c) 2009 Google Inc. All rights reserved.
+# Copyright (c) 2009 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:
+# 
+#     * 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.
+#
+# MultiCommandTool provides a framework for writing svn-like/git-like tools
+# which are called with the following format:
+# tool-name [global options] command-name [command options]
+
+import sys
+
+from optparse import OptionParser, IndentedHelpFormatter, SUPPRESS_USAGE, make_option
+
+from webkitpy.tool.grammar import pluralize
+from webkitpy.common.system.deprecated_logging import log
+
+
+class TryAgain(Exception):
+    pass
+
+
+class Command(object):
+    name = None
+    show_in_main_help = False
+    def __init__(self, help_text, argument_names=None, options=None, long_help=None, requires_local_commits=False):
+        self.help_text = help_text
+        self.long_help = long_help
+        self.argument_names = argument_names
+        self.required_arguments = self._parse_required_arguments(argument_names)
+        self.options = options
+        self.requires_local_commits = requires_local_commits
+        self._tool = None
+        # option_parser can be overriden by the tool using set_option_parser
+        # This default parser will be used for standalone_help printing.
+        self.option_parser = HelpPrintingOptionParser(usage=SUPPRESS_USAGE, add_help_option=False, option_list=self.options)
+
+    def _exit(self, code):
+        sys.exit(code)
+
+    # This design is slightly awkward, but we need the
+    # the tool to be able to create and modify the option_parser
+    # before it knows what Command to run.
+    def set_option_parser(self, option_parser):
+        self.option_parser = option_parser
+        self._add_options_to_parser()
+
+    def _add_options_to_parser(self):
+        options = self.options or []
+        for option in options:
+            self.option_parser.add_option(option)
+
+    # The tool calls bind_to_tool on each Command after adding it to its list.
+    def bind_to_tool(self, tool):
+        # Command instances can only be bound to one tool at a time.
+        if self._tool and tool != self._tool:
+            raise Exception("Command already bound to tool!")
+        self._tool = tool
+
+    @staticmethod
+    def _parse_required_arguments(argument_names):
+        required_args = []
+        if not argument_names:
+            return required_args
+        split_args = argument_names.split(" ")
+        for argument in split_args:
+            if argument[0] == '[':
+                # For now our parser is rather dumb.  Do some minimal validation that
+                # we haven't confused it.
+                if argument[-1] != ']':
+                    raise Exception("Failure to parse argument string %s.  Argument %s is missing ending ]" % (argument_names, argument))
+            else:
+                required_args.append(argument)
+        return required_args
+
+    def name_with_arguments(self):
+        usage_string = self.name
+        if self.options:
+            usage_string += " [options]"
+        if self.argument_names:
+            usage_string += " " + self.argument_names
+        return usage_string
+
+    def parse_args(self, args):
+        return self.option_parser.parse_args(args)
+
+    def check_arguments_and_execute(self, options, args, tool=None):
+        if len(args) < len(self.required_arguments):
+            log("%s required, %s provided.  Provided: %s  Required: %s\nSee '%s help %s' for usage." % (
+                pluralize("argument", len(self.required_arguments)),
+                pluralize("argument", len(args)),
+                "'%s'" % " ".join(args),
+                " ".join(self.required_arguments),
+                tool.name(),
+                self.name))
+            return 1
+        return self.execute(options, args, tool) or 0
+
+    def standalone_help(self):
+        help_text = self.name_with_arguments().ljust(len(self.name_with_arguments()) + 3) + self.help_text + "\n\n"
+        if self.long_help:
+            help_text += "%s\n\n" % self.long_help
+        help_text += self.option_parser.format_option_help(IndentedHelpFormatter())
+        return help_text
+
+    def execute(self, options, args, tool):
+        raise NotImplementedError, "subclasses must implement"
+
+    # main() exists so that Commands can be turned into stand-alone scripts.
+    # Other parts of the code will likely require modification to work stand-alone.
+    def main(self, args=sys.argv):
+        (options, args) = self.parse_args(args)
+        # Some commands might require a dummy tool
+        return self.check_arguments_and_execute(options, args)
+
+
+# FIXME: This should just be rolled into Command.  help_text and argument_names do not need to be instance variables.
+class AbstractDeclarativeCommand(Command):
+    help_text = None
+    argument_names = None
+    long_help = None
+    def __init__(self, options=None, **kwargs):
+        Command.__init__(self, self.help_text, self.argument_names, options=options, long_help=self.long_help, **kwargs)
+
+
+class HelpPrintingOptionParser(OptionParser):
+    def __init__(self, epilog_method=None, *args, **kwargs):
+        self.epilog_method = epilog_method
+        OptionParser.__init__(self, *args, **kwargs)
+
+    def error(self, msg):
+        self.print_usage(sys.stderr)
+        error_message = "%s: error: %s\n" % (self.get_prog_name(), msg)
+        # This method is overriden to add this one line to the output:
+        error_message += "\nType \"%s --help\" to see usage.\n" % self.get_prog_name()
+        self.exit(1, error_message)
+
+    # We override format_epilog to avoid the default formatting which would paragraph-wrap the epilog
+    # and also to allow us to compute the epilog lazily instead of in the constructor (allowing it to be context sensitive).
+    def format_epilog(self, epilog):
+        if self.epilog_method:
+            return "\n%s\n" % self.epilog_method()
+        return ""
+
+
+class HelpCommand(AbstractDeclarativeCommand):
+    name = "help"
+    help_text = "Display information about this program or its subcommands"
+    argument_names = "[COMMAND]"
+
+    def __init__(self):
+        options = [
+            make_option("-a", "--all-commands", action="store_true", dest="show_all_commands", help="Print all available commands"),
+        ]
+        AbstractDeclarativeCommand.__init__(self, options)
+        self.show_all_commands = False # A hack used to pass --all-commands to _help_epilog even though it's called by the OptionParser.
+
+    def _help_epilog(self):
+        # Only show commands which are relevant to this checkout's SCM system.  Might this be confusing to some users?
+        if self.show_all_commands:
+            epilog = "All %prog commands:\n"
+            relevant_commands = self._tool.commands[:]
+        else:
+            epilog = "Common %prog commands:\n"
+            relevant_commands = filter(self._tool.should_show_in_main_help, self._tool.commands)
+        longest_name_length = max(map(lambda command: len(command.name), relevant_commands))
+        relevant_commands.sort(lambda a, b: cmp(a.name, b.name))
+        command_help_texts = map(lambda command: "   %s   %s\n" % (command.name.ljust(longest_name_length), command.help_text), relevant_commands)
+        epilog += "%s\n" % "".join(command_help_texts)
+        epilog += "See '%prog help --all-commands' to list all commands.\n"
+        epilog += "See '%prog help COMMAND' for more information on a specific command.\n"
+        return epilog.replace("%prog", self._tool.name()) # Use of %prog here mimics OptionParser.expand_prog_name().
+
+    # FIXME: This is a hack so that we don't show --all-commands as a global option:
+    def _remove_help_options(self):
+        for option in self.options:
+            self.option_parser.remove_option(option.get_opt_string())
+
+    def execute(self, options, args, tool):
+        if args:
+            command = self._tool.command_by_name(args[0])
+            if command:
+                print command.standalone_help()
+                return 0
+
+        self.show_all_commands = options.show_all_commands
+        self._remove_help_options()
+        self.option_parser.print_help()
+        return 0
+
+
+class MultiCommandTool(object):
+    global_options = None
+
+    def __init__(self, name=None, commands=None):
+        self._name = name or OptionParser(prog=name).get_prog_name() # OptionParser has nice logic for fetching the name.
+        # Allow the unit tests to disable command auto-discovery.
+        self.commands = commands or [cls() for cls in self._find_all_commands() if cls.name]
+        self.help_command = self.command_by_name(HelpCommand.name)
+        # Require a help command, even if the manual test list doesn't include one.
+        if not self.help_command:
+            self.help_command = HelpCommand()
+            self.commands.append(self.help_command)
+        for command in self.commands:
+            command.bind_to_tool(self)
+
+    @classmethod
+    def _add_all_subclasses(cls, class_to_crawl, seen_classes):
+        for subclass in class_to_crawl.__subclasses__():
+            if subclass not in seen_classes:
+                seen_classes.add(subclass)
+                cls._add_all_subclasses(subclass, seen_classes)
+
+    @classmethod
+    def _find_all_commands(cls):
+        commands = set()
+        cls._add_all_subclasses(Command, commands)
+        return sorted(commands)
+
+    def name(self):
+        return self._name
+
+    def _create_option_parser(self):
+        usage = "Usage: %prog [options] COMMAND [ARGS]"
+        return HelpPrintingOptionParser(epilog_method=self.help_command._help_epilog, prog=self.name(), usage=usage)
+
+    @staticmethod
+    def _split_command_name_from_args(args):
+        # Assume the first argument which doesn't start with "-" is the command name.
+        command_index = 0
+        for arg in args:
+            if arg[0] != "-":
+                break
+            command_index += 1
+        else:
+            return (None, args[:])
+
+        command = args[command_index]
+        return (command, args[:command_index] + args[command_index + 1:])
+
+    def command_by_name(self, command_name):
+        for command in self.commands:
+            if command_name == command.name:
+                return command
+        return None
+
+    def path(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def command_completed(self):
+        pass
+
+    def should_show_in_main_help(self, command):
+        return command.show_in_main_help
+
+    def should_execute_command(self, command):
+        return True
+
+    def _add_global_options(self, option_parser):
+        global_options = self.global_options or []
+        for option in global_options:
+            option_parser.add_option(option)
+
+    def handle_global_options(self, options):
+        pass
+
+    def main(self, argv=sys.argv):
+        (command_name, args) = self._split_command_name_from_args(argv[1:])
+
+        option_parser = self._create_option_parser()
+        self._add_global_options(option_parser)
+
+        command = self.command_by_name(command_name) or self.help_command
+        if not command:
+            option_parser.error("%s is not a recognized command" % command_name)
+
+        command.set_option_parser(option_parser)
+        (options, args) = command.parse_args(args)
+        self.handle_global_options(options)
+
+        (should_execute, failure_reason) = self.should_execute_command(command)
+        if not should_execute:
+            log(failure_reason)
+            return 0 # FIXME: Should this really be 0?
+
+        while True:
+            try:
+                result = command.check_arguments_and_execute(options, args, self)
+                break
+            except TryAgain, e:
+                pass
+
+        self.command_completed()
+        return result
diff --git a/Tools/Scripts/webkitpy/tool/multicommandtool_unittest.py b/Tools/Scripts/webkitpy/tool/multicommandtool_unittest.py
new file mode 100644
index 0000000..c19095c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/multicommandtool_unittest.py
@@ -0,0 +1,177 @@
+# Copyright (c) 2009 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 sys
+import unittest
+
+from optparse import make_option
+
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.tool.multicommandtool import MultiCommandTool, Command, TryAgain
+
+
+class TrivialCommand(Command):
+    name = "trivial"
+    show_in_main_help = True
+    def __init__(self, **kwargs):
+        Command.__init__(self, "help text", **kwargs)
+
+    def execute(self, options, args, tool):
+        pass
+
+
+class UncommonCommand(TrivialCommand):
+    name = "uncommon"
+    show_in_main_help = False
+
+
+class LikesToRetry(Command):
+    name = "likes-to-retry"
+    show_in_main_help = True
+
+    def __init__(self, **kwargs):
+        Command.__init__(self, "help text", **kwargs)
+        self.execute_count = 0
+
+    def execute(self, options, args, tool):
+        self.execute_count += 1
+        if self.execute_count < 2:
+            raise TryAgain()
+
+
+class CommandTest(unittest.TestCase):
+    def test_name_with_arguments(self):
+        command_with_args = TrivialCommand(argument_names="ARG1 ARG2")
+        self.assertEqual(command_with_args.name_with_arguments(), "trivial ARG1 ARG2")
+
+        command_with_args = TrivialCommand(options=[make_option("--my_option")])
+        self.assertEqual(command_with_args.name_with_arguments(), "trivial [options]")
+
+    def test_parse_required_arguments(self):
+        self.assertEqual(Command._parse_required_arguments("ARG1 ARG2"), ["ARG1", "ARG2"])
+        self.assertEqual(Command._parse_required_arguments("[ARG1] [ARG2]"), [])
+        self.assertEqual(Command._parse_required_arguments("[ARG1] ARG2"), ["ARG2"])
+        # Note: We might make our arg parsing smarter in the future and allow this type of arguments string.
+        self.assertRaises(Exception, Command._parse_required_arguments, "[ARG1 ARG2]")
+
+    def test_required_arguments(self):
+        two_required_arguments = TrivialCommand(argument_names="ARG1 ARG2 [ARG3]")
+        expected_missing_args_error = "2 arguments required, 1 argument provided.  Provided: 'foo'  Required: ARG1 ARG2\nSee 'trivial-tool help trivial' for usage.\n"
+        exit_code = OutputCapture().assert_outputs(self, two_required_arguments.check_arguments_and_execute, [None, ["foo"], TrivialTool()], expected_stderr=expected_missing_args_error)
+        self.assertEqual(exit_code, 1)
+
+
+class TrivialTool(MultiCommandTool):
+    def __init__(self, commands=None):
+        MultiCommandTool.__init__(self, name="trivial-tool", commands=commands)
+
+    def path(self):
+        return __file__
+
+    def should_execute_command(self, command):
+        return (True, None)
+
+
+class MultiCommandToolTest(unittest.TestCase):
+    def _assert_split(self, args, expected_split):
+        self.assertEqual(MultiCommandTool._split_command_name_from_args(args), expected_split)
+
+    def test_split_args(self):
+        # MultiCommandToolTest._split_command_name_from_args returns: (command, args)
+        full_args = ["--global-option", "command", "--option", "arg"]
+        full_args_expected = ("command", ["--global-option", "--option", "arg"])
+        self._assert_split(full_args, full_args_expected)
+
+        full_args = []
+        full_args_expected = (None, [])
+        self._assert_split(full_args, full_args_expected)
+
+        full_args = ["command", "arg"]
+        full_args_expected = ("command", ["arg"])
+        self._assert_split(full_args, full_args_expected)
+
+    def test_command_by_name(self):
+        # This also tests Command auto-discovery.
+        tool = TrivialTool()
+        self.assertEqual(tool.command_by_name("trivial").name, "trivial")
+        self.assertEqual(tool.command_by_name("bar"), None)
+
+    def _assert_tool_main_outputs(self, tool, main_args, expected_stdout, expected_stderr = "", expected_exit_code=0):
+        exit_code = OutputCapture().assert_outputs(self, tool.main, [main_args], expected_stdout=expected_stdout, expected_stderr=expected_stderr)
+        self.assertEqual(exit_code, expected_exit_code)
+
+    def test_retry(self):
+        likes_to_retry = LikesToRetry()
+        tool = TrivialTool(commands=[likes_to_retry])
+        tool.main(["tool", "likes-to-retry"])
+        self.assertEqual(likes_to_retry.execute_count, 2)
+
+    def test_global_help(self):
+        tool = TrivialTool(commands=[TrivialCommand(), UncommonCommand()])
+        expected_common_commands_help = """Usage: trivial-tool [options] COMMAND [ARGS]
+
+Options:
+  -h, --help  show this help message and exit
+
+Common trivial-tool commands:
+   trivial   help text
+
+See 'trivial-tool help --all-commands' to list all commands.
+See 'trivial-tool help COMMAND' for more information on a specific command.
+
+"""
+        self._assert_tool_main_outputs(tool, ["tool"], expected_common_commands_help)
+        self._assert_tool_main_outputs(tool, ["tool", "help"], expected_common_commands_help)
+        expected_all_commands_help = """Usage: trivial-tool [options] COMMAND [ARGS]
+
+Options:
+  -h, --help  show this help message and exit
+
+All trivial-tool commands:
+   help       Display information about this program or its subcommands
+   trivial    help text
+   uncommon   help text
+
+See 'trivial-tool help --all-commands' to list all commands.
+See 'trivial-tool help COMMAND' for more information on a specific command.
+
+"""
+        self._assert_tool_main_outputs(tool, ["tool", "help", "--all-commands"], expected_all_commands_help)
+        # Test that arguments can be passed before commands as well
+        self._assert_tool_main_outputs(tool, ["tool", "--all-commands", "help"], expected_all_commands_help)
+
+
+    def test_command_help(self):
+        command_with_options = TrivialCommand(options=[make_option("--my_option")], long_help="LONG HELP")
+        tool = TrivialTool(commands=[command_with_options])
+        expected_subcommand_help = "trivial [options]   help text\n\nLONG HELP\n\nOptions:\n  --my_option=MY_OPTION\n\n"
+        self._assert_tool_main_outputs(tool, ["tool", "help", "trivial"], expected_subcommand_help)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/Tools/Scripts/webkitpy/tool/servers/__init__.py b/Tools/Scripts/webkitpy/tool/servers/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/servers/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/index.html b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/index.html
new file mode 100644
index 0000000..f40a34d
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/index.html
@@ -0,0 +1,182 @@
+<!DOCTYPE html>
+<!--
+  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.
+-->
+<html>
+<head>
+  <title>Layout Test Rebaseline Server</title>
+  <link rel="stylesheet" href="/main.css" type="text/css">
+  <script src="/util.js"></script>
+  <script src="/loupe.js"></script>
+  <script src="/main.js"></script>
+  <script src="/queue.js"></script>
+</head>
+<body class="loading">
+
+<pre id="log" style="display: none"></pre>
+<div id="queue" style="display: none">
+  Queue:
+  <select id="queue-select" size="10"></select>
+  <button id="remove-queue-selection">Remove selection</button>
+  <button id="rebaseline-queue">Rebaseline queue</button>
+</div>
+
+<div id="header">
+  <div id="controls">
+    <!-- Add a dummy <select> node so that this lines up with the text on the left -->
+    <select style="visibility: hidden"></select>
+    <span id="toggle-sort" class="link">Sort tests by metric</span>
+    <span class="divider">|</span>
+    <span id="toggle-log" class="link">Log</span>
+    <span class="divider">|</span>
+    <a href="/quitquitquit">Exit</a>
+  </div>
+
+  <span id="selectors">
+    <label>
+      Failure type:
+      <select id="failure-type-selector"></select>
+    </label>
+
+    <label>
+      Directory:
+      <select id="directory-selector"></select>
+    </label>
+
+    <label>
+      Test:
+      <select id="test-selector"></select>
+    </label>
+  </span>
+
+  <a id="test-link" target="_blank">View test</a>
+
+  <span id="nav-buttons">
+    <button id="previous-test">&laquo;</button>
+    <span id="test-index"></span> of <span id="test-count"></span>
+    <button id="next-test">&raquo;</button>
+  </span>
+</div>
+
+<table id="test-output">
+  <thead id="labels">
+    <tr>
+      <th>Expected</th>
+      <th>Actual</th>
+      <th>Diff</th>
+    </tr>
+  </thead>
+  <tbody id="image-outputs" style="display: none">
+    <tr>
+      <td colspan="3"><h2>Image</h2></td>
+    </tr>
+    <tr>
+      <td><img id="expected-image"></td>
+      <td><img id="actual-image"></td>
+      <td>
+        <canvas id="diff-canvas" width="800" height="600"></canvas>
+        <div id="diff-checksum" style="display: none">
+          <h3>Checksum mismatch</h3>
+          Expected: <span id="expected-checksum"></span><br>
+          Actual: <span id="actual-checksum"></span>
+        </div>
+      </td>
+    </tr>
+  </tbody>
+  <tbody id="text-outputs" style="display: none">
+    <tr>
+      <td colspan="3"><h2>Text</h2></td>
+    </tr>
+    <tr>
+      <td><pre id="expected-text" class="text-output"></pre></td>
+      <td><pre id="actual-text" class="text-output"></pre></td>
+      <td><div id="diff-text-pretty" class="text-output"></div></td>
+    </tr>
+  </tbody>
+</table>
+
+<div id="footer">
+  <label>State: <span id="state"></span></label>
+  <label>Existing baselines: <span id="current-baselines"></span></label>
+  <label>
+    Baseline target:
+    <select id="baseline-target"></select>
+  </label>
+  <label>
+    Move current baselines to:
+    <select id="baseline-move-to">
+      <option value="none">Nowhere (replace)</option>
+    </select>
+  </label>
+
+  <!-- Add a dummy <button> node so that this lines up with the text on the right -->
+  <button style="visibility: hidden; padding-left: 0; padding-right: 0;"></button>
+
+  <div id="action-buttons">
+    <span id="toggle-queue" class="link">Queue</span>
+    <button id="add-to-rebaseline-queue">Add to rebaseline queue</button>
+  </div>
+</div>
+
+<table id="loupe" style="display: none">
+  <tr>
+    <td colspan="3" id="loupe-info">
+      <span id="loupe-close" class="link">Close</span>
+      <label>Coordinate: <span id="loupe-coordinate"></span></label>
+    </td>
+  </tr>
+  <tr>
+    <td>
+      <div class="loupe-container">
+        <canvas id="expected-loupe" width="210" height="210"></canvas>
+        <div class="center-highlight"></div>
+      </div>
+    </td>
+    <td>
+      <div class="loupe-container">
+        <canvas id="actual-loupe" width="210" height="210"></canvas>
+        <div class="center-highlight"></div>
+      </div>
+    </td>
+    <td>
+      <div class="loupe-container">
+        <canvas id="diff-loupe" width="210" height="210"></canvas>
+        <div class="center-highlight"></div>
+      </div>
+    </td>
+  </tr>
+  <tr id="loupe-colors">
+    <td><label>Exp. color: <span id="expected-loupe-color"></span></label></td>
+    <td><label>Actual color: <span id="actual-loupe-color"></span></label></td>
+    <td><label>Diff color: <span id="diff-loupe-color"></span></label></td>
+  </tr>
+</table>
+
+</body>
+</html>
diff --git a/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/loupe.js b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/loupe.js
new file mode 100644
index 0000000..41f977a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/loupe.js
@@ -0,0 +1,144 @@
+/*
+ * 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.
+ */
+
+var LOUPE_MAGNIFICATION_FACTOR = 10;
+
+function Loupe()
+{
+    this._node = $('loupe');
+    this._currentCornerX = -1;
+    this._currentCornerY = -1;
+
+    var self = this;
+
+    function handleOutputClick(event) { self._handleOutputClick(event); }
+    $('expected-image').addEventListener('click', handleOutputClick);
+    $('actual-image').addEventListener('click', handleOutputClick);
+    $('diff-canvas').addEventListener('click', handleOutputClick);
+
+    function handleLoupeClick(event) { self._handleLoupeClick(event); }
+    $('expected-loupe').addEventListener('click', handleLoupeClick);
+    $('actual-loupe').addEventListener('click', handleLoupeClick);
+    $('diff-loupe').addEventListener('click', handleLoupeClick);
+
+    function hide(event) { self.hide(); }
+    $('loupe-close').addEventListener('click', hide);
+}
+
+Loupe.prototype._handleOutputClick = function(event)
+{
+    // The -1 compensates for the border around the image/canvas.
+    this._showFor(event.offsetX - 1, event.offsetY - 1);
+};
+
+Loupe.prototype._handleLoupeClick = function(event)
+{
+    var deltaX = Math.floor(event.offsetX/LOUPE_MAGNIFICATION_FACTOR);
+    var deltaY = Math.floor(event.offsetY/LOUPE_MAGNIFICATION_FACTOR);
+
+    this._showFor(
+        this._currentCornerX + deltaX, this._currentCornerY + deltaY);
+}
+
+Loupe.prototype.hide = function()
+{
+    this._node.style.display = 'none';
+};
+
+Loupe.prototype._showFor = function(x, y)
+{
+    this._fillFromImage(x, y, 'expected', $('expected-image'));
+    this._fillFromImage(x, y, 'actual', $('actual-image'));
+    this._fillFromCanvas(x, y, 'diff', $('diff-canvas'));
+
+    this._node.style.display = '';
+};
+
+Loupe.prototype._fillFromImage = function(x, y, type, sourceImage)
+{
+    var tempCanvas = document.createElement('canvas');
+    tempCanvas.width = sourceImage.width;
+    tempCanvas.height = sourceImage.height;
+    var tempContext = tempCanvas.getContext('2d');
+
+    tempContext.drawImage(sourceImage, 0, 0);
+
+    this._fillFromCanvas(x, y, type, tempCanvas);
+};
+
+Loupe.prototype._fillFromCanvas = function(x, y, type, canvas)
+{
+    var context = canvas.getContext('2d');
+    var sourceImageData =
+        context.getImageData(0, 0, canvas.width, canvas.height);
+
+    var targetCanvas = $(type + '-loupe');
+    var targetContext = targetCanvas.getContext('2d');
+    targetContext.fillStyle = 'rgba(255, 255, 255, 1)';
+    targetContext.fillRect(0, 0, targetCanvas.width, targetCanvas.height);
+
+    var sourceXOffset = (targetCanvas.width/LOUPE_MAGNIFICATION_FACTOR - 1)/2;
+    var sourceYOffset = (targetCanvas.height/LOUPE_MAGNIFICATION_FACTOR - 1)/2;
+
+    function readPixelComponent(x, y, component) {
+        var offset = (y * sourceImageData.width + x) * 4 + component;
+        return sourceImageData.data[offset];
+    }
+
+    for (var i = -sourceXOffset; i <= sourceXOffset; i++) {
+        for (var j = -sourceYOffset; j <= sourceYOffset; j++) {
+            var sourceX = x + i;
+            var sourceY = y + j;
+
+            var sourceR = readPixelComponent(sourceX, sourceY, 0);
+            var sourceG = readPixelComponent(sourceX, sourceY, 1);
+            var sourceB = readPixelComponent(sourceX, sourceY, 2);
+            var sourceA = readPixelComponent(sourceX, sourceY, 3)/255;
+            sourceA = Math.round(sourceA * 10)/10;
+
+            var targetX = (i + sourceXOffset) * LOUPE_MAGNIFICATION_FACTOR;
+            var targetY = (j + sourceYOffset) * LOUPE_MAGNIFICATION_FACTOR;
+            var colorString =
+                sourceR + ', ' + sourceG + ', ' + sourceB + ', ' + sourceA;
+            targetContext.fillStyle = 'rgba(' + colorString + ')';
+            targetContext.fillRect(
+                targetX, targetY,
+                LOUPE_MAGNIFICATION_FACTOR, LOUPE_MAGNIFICATION_FACTOR);
+
+            if (i == 0 && j == 0) {
+                $('loupe-coordinate').textContent = sourceX + ', ' + sourceY;
+                $(type + '-loupe-color').textContent = colorString;
+            }
+        }
+    }
+
+    this._currentCornerX = x - sourceXOffset;
+    this._currentCornerY = y - sourceYOffset;
+};
diff --git a/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/main.css b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/main.css
new file mode 100644
index 0000000..280c3b2
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/main.css
@@ -0,0 +1,313 @@
+/*
+ * 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.
+ */
+
+body {
+  font-size: 12px;
+  font-family: Helvetica, Arial, sans-serif;
+  padding: 0;
+  margin: 0;
+}
+
+.loading {
+  opacity: 0.5;
+}
+
+div {
+  margin: 0;
+}
+
+a, .link {
+  color: #aaf;
+  text-decoration: underline;
+  cursor: pointer;
+}
+
+.link.selected {
+  color: #fff;
+  font-weight: bold;
+  text-decoration: none;
+}
+
+#log,
+#queue {
+  padding: .25em 0 0 .25em;
+  position: absolute;
+  right: 0;
+  height: 200px;
+  overflow: auto;
+  background: #fff;
+  -webkit-box-shadow: 1px 1px 5px rgba(0, 0, 0, .5);
+}
+
+#log {
+  top: 2em;
+  width: 500px;
+}
+
+#queue {
+  bottom: 3em;
+  width: 400px;
+}
+
+#queue-select {
+  display: block;
+  width: 390px;
+}
+
+#header,
+#footer {
+  padding: .5em 1em;
+  background: #333;
+  color: #fff;
+  -webkit-box-shadow: 0 1px 5px rgba(0, 0, 0, 0.5);
+}
+
+#header {
+  margin-bottom: 1em;
+}
+
+#header .divider,
+#footer .divider {
+  opacity: .3;
+  padding: 0 .5em;
+}
+
+#header label,
+#footer label {
+  padding-right: 1em;
+  color: #ccc;
+}
+
+#test-link {
+  margin-right: 1em;
+}
+
+#header label span,
+#footer label span {
+  color: #fff;
+  font-weight: bold;
+}
+
+#nav-buttons {
+  white-space: nowrap;
+}
+
+#nav-buttons button {
+  background: #fff;
+  border: 0;
+  border-radius: 10px;
+}
+
+#nav-buttons button:active {
+  -webkit-box-shadow: 0 0 5px #33f inset;
+  background: #aaa;
+}
+
+#nav-buttons button[disabled] {
+  opacity: .5;
+}
+
+#controls {
+  float: right;
+}
+
+.disabled-control {
+  color: #888;
+}
+
+#test-output {
+  border-spacing: 0;
+  border-collapse: collapse;
+  margin: 0 auto;
+  width: 100%;
+}
+
+#test-output td,
+#test-output th {
+  padding: 0;
+  vertical-align: top;
+}
+
+#image-outputs img,
+#image-outputs canvas,
+#image-outputs #diff-checksum {
+  width: 800px;
+  height: 600px;
+  border: solid 1px #ddd;
+  -webkit-user-select: none;
+  -webkit-user-drag: none;
+}
+
+#image-outputs img,
+#image-outputs canvas {
+  cursor: crosshair;
+}
+
+#image-outputs img.loading,
+#image-outputs canvas.loading {
+  opacity: .5;
+}
+
+#image-outputs #actual-image {
+  margin: 0 1em;
+}
+
+#test-output #labels th {
+  text-align: center;
+  color: #666;
+}
+
+#text-outputs .text-output {
+  height: 600px;
+  width: 800px;
+  overflow: auto;
+}
+
+#test-output h2 {
+  border-bottom: solid 1px #ccc;
+  font-weight: bold;
+  margin: 0;
+  background: #eee;
+}
+
+#footer {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  margin-top: 1em;
+}
+
+#state.needs_rebaseline {
+  color: yellow;
+}
+
+#state.rebaseline_failed {
+  color: red;
+}
+
+#state.rebaseline_succeeded {
+  color: green;
+}
+
+#state.in_queue {
+  color: gray;
+}
+
+#current-baselines {
+  font-weight: normal !important;
+}
+
+#current-baselines .platform {
+  font-weight: bold;
+}
+
+#current-baselines a {
+  color: #ddf;
+}
+
+#current-baselines .was-used-for-test {
+  color: #aaf;
+  font-weight: bold;
+}
+
+#action-buttons {
+  float: right;
+}
+
+#action-buttons .link {
+  margin-right: 1em;
+}
+
+#footer button {
+  padding: 1em;
+}
+
+#loupe {
+  -webkit-box-shadow: 2px 2px 5px rgba(0, 0, 0, .5);
+  position: absolute;
+  width: 634px;
+  top: 50%;
+  left: 50%;
+  margin-left: -151px;
+  margin-top: -50px;
+  background: #fff;
+  border-spacing: 0;
+  border-collapse: collapse;
+}
+
+#loupe td {
+  padding: 0;
+  border: solid 1px #ccc;
+}
+
+#loupe label {
+  color: #999;
+  padding-right: 1em;
+}
+
+#loupe span {
+  color: #000;
+  font-weight: bold;
+}
+
+#loupe canvas {
+  cursor: crosshair;
+}
+
+#loupe #loupe-close {
+  float: right;
+}
+
+#loupe #loupe-info {
+  background: #eee;
+  padding: .3em .5em;
+}
+
+#loupe #loupe-colors td {
+  text-align: center;
+}
+
+#loupe .loupe-container {
+  position: relative;
+  width: 210px;
+  height: 210px;
+}
+
+#loupe .center-highlight {
+  position: absolute;
+  width: 10px;
+  height: 10px;
+  top: 50%;
+  left: 50%;
+  margin-left: -5px;
+  margin-top: -5px;
+  outline: solid 1px #999;
+}
diff --git a/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/main.js b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/main.js
new file mode 100644
index 0000000..5e1fa52
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/main.js
@@ -0,0 +1,577 @@
+/*
+ * 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.
+ */
+
+var ALL_DIRECTORY_PATH = '[all]';
+
+var STATE_NEEDS_REBASELINE = 'needs_rebaseline';
+var STATE_REBASELINE_FAILED = 'rebaseline_failed';
+var STATE_REBASELINE_SUCCEEDED = 'rebaseline_succeeded';
+var STATE_IN_QUEUE = 'in_queue';
+var STATE_TO_DISPLAY_STATE = {};
+STATE_TO_DISPLAY_STATE[STATE_NEEDS_REBASELINE] = 'Needs rebaseline';
+STATE_TO_DISPLAY_STATE[STATE_REBASELINE_FAILED] = 'Rebaseline failed';
+STATE_TO_DISPLAY_STATE[STATE_REBASELINE_SUCCEEDED] = 'Rebaseline succeeded';
+STATE_TO_DISPLAY_STATE[STATE_IN_QUEUE] = 'In queue';
+
+var results;
+var testsByFailureType = {};
+var testsByDirectory = {};
+var selectedTests = [];
+var loupe;
+var queue;
+var shouldSortTestsByMetric = false;
+
+function main()
+{
+    $('failure-type-selector').addEventListener('change', selectFailureType);
+    $('directory-selector').addEventListener('change', selectDirectory);
+    $('test-selector').addEventListener('change', selectTest);
+    $('next-test').addEventListener('click', nextTest);
+    $('previous-test').addEventListener('click', previousTest);
+
+    $('toggle-log').addEventListener('click', function() { toggle('log'); });
+    disableSorting();
+
+    loupe = new Loupe();
+    queue = new RebaselineQueue();
+
+    document.addEventListener('keydown', function(event) {
+        if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
+            return;
+        }
+
+        switch (event.keyIdentifier) {
+        case 'Left':
+            event.preventDefault();
+            previousTest();
+            break;
+        case 'Right':
+            event.preventDefault();
+            nextTest();
+            break;
+        case 'U+0051': // q
+            queue.addCurrentTest();
+            break;
+        case 'U+0058': // x
+            queue.removeCurrentTest();
+            break;
+        case 'U+0052': // r
+            queue.rebaseline();
+            break;
+        }
+    });
+
+    loadText('/platforms.json', function(text) {
+        var platforms = JSON.parse(text);
+        platforms.platforms.forEach(function(platform) {
+            var platformOption = document.createElement('option');
+            platformOption.value = platform;
+            platformOption.textContent = platform;
+
+            var targetOption = platformOption.cloneNode(true);
+            targetOption.selected = platform == platforms.defaultPlatform;
+            $('baseline-target').appendChild(targetOption);
+            $('baseline-move-to').appendChild(platformOption.cloneNode(true));
+        });
+    });
+
+    loadText('/results.json', function(text) {
+        results = JSON.parse(text);
+        displayResults();
+    });
+}
+
+/**
+ * Groups test results by failure type.
+ */
+function displayResults()
+{
+    var failureTypeSelector = $('failure-type-selector');
+    var failureTypes = [];
+
+    for (var testName in results.tests) {
+        var test = results.tests[testName];
+        if (test.actual == 'PASS') {
+            continue;
+        }
+        var failureType = test.actual + ' (expected ' + test.expected + ')';
+        if (!(failureType in testsByFailureType)) {
+            testsByFailureType[failureType] = [];
+            failureTypes.push(failureType);
+        }
+        testsByFailureType[failureType].push(testName);
+    }
+
+    // Sort by number of failures
+    failureTypes.sort(function(a, b) {
+        return testsByFailureType[b].length - testsByFailureType[a].length;
+    });
+
+    for (var i = 0, failureType; failureType = failureTypes[i]; i++) {
+        var failureTypeOption = document.createElement('option');
+        failureTypeOption.value = failureType;
+        failureTypeOption.textContent = failureType + ' - ' + testsByFailureType[failureType].length + ' tests';
+        failureTypeSelector.appendChild(failureTypeOption);
+    }
+
+    selectFailureType();
+
+    document.body.className = '';
+}
+
+function enableSorting()
+{
+    $('toggle-sort').onclick = function() {
+        shouldSortTestsByMetric = !shouldSortTestsByMetric;
+        // Regenerates the list of tests; this alphabetizes, and
+        // then re-sorts if we turned sorting on.
+        selectDirectory();
+    }
+    $('toggle-sort').classList.remove('disabled-control');
+}
+
+function disableSorting()
+{
+    $('toggle-sort').onclick = function() { return false; }
+    $('toggle-sort').classList.add('disabled-control');
+}
+
+/**
+ * For a given failure type, gets all the tests and groups them by directory
+ * (populating the directory selector with them).
+ */
+function selectFailureType()
+{
+    var selectedFailureType = getSelectValue('failure-type-selector');
+    var tests = testsByFailureType[selectedFailureType];
+
+    testsByDirectory = {}
+    var displayDirectoryNamesByDirectory = {};
+    var directories = [];
+
+    // Include a special option for all tests
+    testsByDirectory[ALL_DIRECTORY_PATH] = tests;
+    displayDirectoryNamesByDirectory[ALL_DIRECTORY_PATH] = 'all';
+    directories.push(ALL_DIRECTORY_PATH);
+
+    // Roll up tests by ancestor directories
+    tests.forEach(function(test) {
+        var pathPieces = test.split('/');
+        var pathDirectories = pathPieces.slice(0, pathPieces.length -1);
+        var ancestorDirectory = '';
+
+        pathDirectories.forEach(function(pathDirectory, index) {
+            ancestorDirectory += pathDirectory + '/';
+            if (!(ancestorDirectory in testsByDirectory)) {
+                testsByDirectory[ancestorDirectory] = [];
+                var displayDirectoryName = new Array(index * 6).join('&nbsp;') + pathDirectory;
+                displayDirectoryNamesByDirectory[ancestorDirectory] = displayDirectoryName;
+                directories.push(ancestorDirectory);
+            }
+
+            testsByDirectory[ancestorDirectory].push(test);
+        });
+    });
+
+    directories.sort();
+
+    var directorySelector = $('directory-selector');
+    directorySelector.innerHTML = '';
+
+    directories.forEach(function(directory) {
+        var directoryOption = document.createElement('option');
+        directoryOption.value = directory;
+        directoryOption.innerHTML =
+            displayDirectoryNamesByDirectory[directory] + ' - ' +
+            testsByDirectory[directory].length + ' tests';
+        directorySelector.appendChild(directoryOption);
+    });
+
+    selectDirectory();
+}
+
+/**
+ * For a given failure type and directory and failure type, gets all the tests
+ * in that directory and populatest the test selector with them.
+ */
+function selectDirectory()
+{
+    var previouslySelectedTest = getSelectedTest();
+
+    var selectedDirectory = getSelectValue('directory-selector');
+    selectedTests = testsByDirectory[selectedDirectory];
+    selectedTests.sort();
+
+    var testsByState = {};
+    selectedTests.forEach(function(testName) {
+        var state = results.tests[testName].state;
+        if (state == STATE_IN_QUEUE) {
+            state = STATE_NEEDS_REBASELINE;
+        }
+        if (!(state in testsByState)) {
+            testsByState[state] = [];
+        }
+        testsByState[state].push(testName);
+    });
+
+    var optionIndexByTest = {};
+
+    var testSelector = $('test-selector');
+    testSelector.innerHTML = '';
+
+    var selectedFailureType = getSelectValue('failure-type-selector');
+    var sampleSelectedTest = testsByFailureType[selectedFailureType][0];
+    var selectedTypeIsSortable = 'metric' in results.tests[sampleSelectedTest];
+    if (selectedTypeIsSortable) {
+        enableSorting();
+        if (shouldSortTestsByMetric) {
+            for (var state in testsByState) {
+                testsByState[state].sort(function(a, b) {
+                    return results.tests[b].metric - results.tests[a].metric
+                })
+            }
+        }
+    } else
+        disableSorting();
+
+    for (var state in testsByState) {
+        var stateOption = document.createElement('option');
+        stateOption.textContent = STATE_TO_DISPLAY_STATE[state];
+        stateOption.disabled = true;
+        testSelector.appendChild(stateOption);
+
+        testsByState[state].forEach(function(testName) {
+            var testOption = document.createElement('option');
+            testOption.value = testName;
+            var testDisplayName = testName;
+            if (testName.lastIndexOf(selectedDirectory) == 0) {
+                testDisplayName = testName.substring(selectedDirectory.length);
+            }
+            testOption.innerHTML = '&nbsp;&nbsp;' + testDisplayName;
+            optionIndexByTest[testName] = testSelector.options.length;
+            testSelector.appendChild(testOption);
+        });
+    }
+
+    if (previouslySelectedTest in optionIndexByTest) {
+        testSelector.selectedIndex = optionIndexByTest[previouslySelectedTest];
+    } else if (STATE_NEEDS_REBASELINE in testsByState) {
+        testSelector.selectedIndex =
+            optionIndexByTest[testsByState[STATE_NEEDS_REBASELINE][0]];
+        selectTest();
+    } else {
+        testSelector.selectedIndex = 1;
+        selectTest();
+    }
+
+    selectTest();
+}
+
+function getSelectedTest()
+{
+    return getSelectValue('test-selector');
+}
+
+function selectTest()
+{
+    var selectedTest = getSelectedTest();
+
+    if (results.tests[selectedTest].actual.indexOf('IMAGE') != -1) {
+        $('image-outputs').style.display = '';
+        displayImageResults(selectedTest);
+    } else {
+        $('image-outputs').style.display = 'none';
+    }
+
+    if (results.tests[selectedTest].actual.indexOf('TEXT') != -1) {
+        $('text-outputs').style.display = '';
+        displayTextResults(selectedTest);
+    } else {
+        $('text-outputs').style.display = 'none';
+    }
+
+    var currentBaselines = $('current-baselines');
+    currentBaselines.textContent = '';
+    var baselines = results.tests[selectedTest].baselines;
+    var testName = selectedTest.split('.').slice(0, -1).join('.');
+    getSortedKeys(baselines).forEach(function(platform, i) {
+        if (i != 0) {
+            currentBaselines.appendChild(document.createTextNode('; '));
+        }
+        var platformName = document.createElement('span');
+        platformName.className = 'platform';
+        platformName.textContent = platform;
+        currentBaselines.appendChild(platformName);
+        currentBaselines.appendChild(document.createTextNode(' ('));
+        getSortedKeys(baselines[platform]).forEach(function(extension, j) {
+            if (j != 0) {
+                currentBaselines.appendChild(document.createTextNode(', '));
+            }
+            var link = document.createElement('a');
+            var baselinePath = '';
+            if (platform != 'base') {
+                baselinePath += 'platform/' + platform + '/';
+            }
+            baselinePath += testName + '-expected' + extension;
+            link.href = getTracUrl(baselinePath);
+            if (extension == '.checksum') {
+                link.textContent = 'chk';
+            } else {
+                link.textContent = extension.substring(1);
+            }
+            link.target = '_blank';
+            if (baselines[platform][extension]) {
+                link.className = 'was-used-for-test';
+            }
+            currentBaselines.appendChild(link);
+        });
+        currentBaselines.appendChild(document.createTextNode(')'));
+    });
+
+    updateState();
+    loupe.hide();
+
+    prefetchNextImageTest();
+}
+
+function prefetchNextImageTest()
+{
+    var testSelector = $('test-selector');
+    if (testSelector.selectedIndex == testSelector.options.length - 1) {
+        return;
+    }
+    var nextTest = testSelector.options[testSelector.selectedIndex + 1].value;
+    if (results.tests[nextTest].actual.indexOf('IMAGE') != -1) {
+        new Image().src = getTestResultUrl(nextTest, 'expected-image');
+        new Image().src = getTestResultUrl(nextTest, 'actual-image');
+    }
+}
+
+function updateState()
+{
+    var testName = getSelectedTest();
+    var testIndex = selectedTests.indexOf(testName);
+    var testCount = selectedTests.length
+    $('test-index').textContent = testIndex + 1;
+    $('test-count').textContent = testCount;
+
+    $('next-test').disabled = testIndex == testCount - 1;
+    $('previous-test').disabled = testIndex == 0;
+
+    $('test-link').href = getTracUrl(testName);
+
+    var state = results.tests[testName].state;
+    $('state').className = state;
+    $('state').innerHTML = STATE_TO_DISPLAY_STATE[state];
+
+    queue.updateState();
+}
+
+function getTestResultUrl(testName, mode)
+{
+    return '/test_result?test=' + testName + '&mode=' + mode;
+}
+
+var currentExpectedImageTest;
+var currentActualImageTest;
+
+function displayImageResults(testName)
+{
+    if (currentExpectedImageTest == currentActualImageTest
+        && currentExpectedImageTest == testName) {
+        return;
+    }
+
+    function displayImageResult(mode, callback) {
+        var image = $(mode);
+        image.className = 'loading';
+        image.src = getTestResultUrl(testName, mode);
+        image.onload = function() {
+            image.className = '';
+            callback();
+            updateImageDiff();
+        };
+    }
+
+    displayImageResult(
+        'expected-image',
+        function() { currentExpectedImageTest = testName; });
+    displayImageResult(
+        'actual-image',
+        function() { currentActualImageTest = testName; });
+
+    $('diff-canvas').className = 'loading';
+    $('diff-canvas').style.display = '';
+    $('diff-checksum').style.display = 'none';
+}
+
+/**
+ * Computes a graphical a diff between the expected and actual images by
+ * rendering each to a canvas, getting the image data, and comparing the RGBA
+ * components of each pixel. The output is put into the diff canvas, with
+ * identical pixels appearing at 12.5% opacity and different pixels being
+ * highlighted in red.
+ */
+function updateImageDiff() {
+    if (currentExpectedImageTest != currentActualImageTest)
+        return;
+
+    var expectedImage = $('expected-image');
+    var actualImage = $('actual-image');
+
+    function getImageData(image) {
+        var imageCanvas = document.createElement('canvas');
+        imageCanvas.width = image.width;
+        imageCanvas.height = image.height;
+        imageCanvasContext = imageCanvas.getContext('2d');
+
+        imageCanvasContext.fillStyle = 'rgba(255, 255, 255, 1)';
+        imageCanvasContext.fillRect(
+            0, 0, image.width, image.height);
+
+        imageCanvasContext.drawImage(image, 0, 0);
+        return imageCanvasContext.getImageData(
+            0, 0, image.width, image.height);
+    }
+
+    var expectedImageData = getImageData(expectedImage);
+    var actualImageData = getImageData(actualImage);
+
+    var diffCanvas = $('diff-canvas');
+    var diffCanvasContext = diffCanvas.getContext('2d');
+    var diffImageData =
+        diffCanvasContext.createImageData(diffCanvas.width, diffCanvas.height);
+
+    // Avoiding property lookups for all these during the per-pixel loop below
+    // provides a significant performance benefit.
+    var expectedWidth = expectedImage.width;
+    var expectedHeight = expectedImage.height;
+    var expected = expectedImageData.data;
+
+    var actualWidth = actualImage.width;
+    var actual = actualImageData.data;
+
+    var diffWidth = diffImageData.width;
+    var diff = diffImageData.data;
+
+    var hadDiff = false;
+    for (var x = 0; x < expectedWidth; x++) {
+        for (var y = 0; y < expectedHeight; y++) {
+            var expectedOffset = (y * expectedWidth + x) * 4;
+            var actualOffset = (y * actualWidth + x) * 4;
+            var diffOffset = (y * diffWidth + x) * 4;
+            if (expected[expectedOffset] != actual[actualOffset] ||
+                expected[expectedOffset + 1] != actual[actualOffset + 1] ||
+                expected[expectedOffset + 2] != actual[actualOffset + 2] ||
+                expected[expectedOffset + 3] != actual[actualOffset + 3]) {
+                hadDiff = true;
+                diff[diffOffset] = 255;
+                diff[diffOffset + 1] = 0;
+                diff[diffOffset + 2] = 0;
+                diff[diffOffset + 3] = 255;
+            } else {
+                diff[diffOffset] = expected[expectedOffset];
+                diff[diffOffset + 1] = expected[expectedOffset + 1];
+                diff[diffOffset + 2] = expected[expectedOffset + 2];
+                diff[diffOffset + 3] = 32;
+            }
+        }
+    }
+
+    diffCanvasContext.putImageData(
+        diffImageData,
+        0, 0,
+        0, 0,
+        diffImageData.width, diffImageData.height);
+    diffCanvas.className = '';
+
+    if (!hadDiff) {
+        diffCanvas.style.display = 'none';
+        $('diff-checksum').style.display = '';
+        loadTextResult(currentExpectedImageTest, 'expected-checksum');
+        loadTextResult(currentExpectedImageTest, 'actual-checksum');
+    }
+}
+
+function loadTextResult(testName, mode, responseIsHtml)
+{
+    loadText(getTestResultUrl(testName, mode), function(text) {
+        if (responseIsHtml) {
+            $(mode).innerHTML = text;
+        } else {
+            $(mode).textContent = text;
+        }
+    });
+}
+
+function displayTextResults(testName)
+{
+    loadTextResult(testName, 'expected-text');
+    loadTextResult(testName, 'actual-text');
+    loadTextResult(testName, 'diff-text-pretty', true);
+}
+
+function nextTest()
+{
+    var testSelector = $('test-selector');
+    var nextTestIndex = testSelector.selectedIndex + 1;
+    while (true) {
+        if (nextTestIndex == testSelector.options.length) {
+            return;
+        }
+        if (testSelector.options[nextTestIndex].disabled) {
+            nextTestIndex++;
+        } else {
+            testSelector.selectedIndex = nextTestIndex;
+            selectTest();
+            return;
+        }
+    }
+}
+
+function previousTest()
+{
+    var testSelector = $('test-selector');
+    var previousTestIndex = testSelector.selectedIndex - 1;
+    while (true) {
+        if (previousTestIndex == -1) {
+            return;
+        }
+        if (testSelector.options[previousTestIndex].disabled) {
+            previousTestIndex--;
+        } else {
+            testSelector.selectedIndex = previousTestIndex;
+            selectTest();
+            return
+        }
+    }
+}
+
+window.addEventListener('DOMContentLoaded', main);
diff --git a/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/queue.js b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/queue.js
new file mode 100644
index 0000000..338e28f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/queue.js
@@ -0,0 +1,186 @@
+/*
+ * 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.
+ */
+
+function RebaselineQueue()
+{
+    this._selectNode = $('queue-select');
+    this._rebaselineButtonNode = $('rebaseline-queue');
+    this._toggleNode = $('toggle-queue');
+    this._removeSelectionButtonNode = $('remove-queue-selection');
+
+    this._inProgressRebaselineCount = 0;
+
+    var self = this;
+    $('add-to-rebaseline-queue').addEventListener(
+        'click', function() { self.addCurrentTest(); });
+    this._selectNode.addEventListener('change', updateState);
+    this._removeSelectionButtonNode.addEventListener(
+        'click', function() { self._removeSelection(); });
+    this._rebaselineButtonNode.addEventListener(
+        'click', function() { self.rebaseline(); });
+    this._toggleNode.addEventListener(
+        'click', function() { toggle('queue'); });
+}
+
+RebaselineQueue.prototype.updateState = function()
+{
+    var testName = getSelectedTest();
+
+    var state = results.tests[testName].state;
+    $('add-to-rebaseline-queue').disabled = state != STATE_NEEDS_REBASELINE;
+
+    var queueLength = this._selectNode.options.length;
+    if (this._inProgressRebaselineCount > 0) {
+      this._rebaselineButtonNode.disabled = true;
+      this._rebaselineButtonNode.textContent =
+          'Rebaseline in progress (' + this._inProgressRebaselineCount +
+          ' tests left)';
+    } else if (queueLength == 0) {
+      this._rebaselineButtonNode.disabled = true;
+      this._rebaselineButtonNode.textContent = 'Rebaseline queue';
+      this._toggleNode.textContent = 'Queue';
+    } else {
+      this._rebaselineButtonNode.disabled = false;
+      this._rebaselineButtonNode.textContent =
+          'Rebaseline queue (' + queueLength + ' tests)';
+      this._toggleNode.textContent = 'Queue (' + queueLength + ' tests)';
+    }
+    this._removeSelectionButtonNode.disabled =
+        this._selectNode.selectedIndex == -1;
+};
+
+RebaselineQueue.prototype.addCurrentTest = function()
+{
+    var testName = getSelectedTest();
+    var test = results.tests[testName];
+
+    if (test.state != STATE_NEEDS_REBASELINE) {
+        log('Cannot add test with state "' + test.state + '" to queue.',
+            log.WARNING);
+        return;
+    }
+
+    var queueOption = document.createElement('option');
+    queueOption.value = testName;
+    queueOption.textContent = testName;
+    this._selectNode.appendChild(queueOption);
+    test.state = STATE_IN_QUEUE;
+    updateState();
+};
+
+RebaselineQueue.prototype.removeCurrentTest = function()
+{
+    this._removeTest(getSelectedTest());
+};
+
+RebaselineQueue.prototype._removeSelection = function()
+{
+    if (this._selectNode.selectedIndex == -1)
+        return;
+
+    this._removeTest(
+        this._selectNode.options[this._selectNode.selectedIndex].value);
+};
+
+RebaselineQueue.prototype._removeTest = function(testName)
+{
+    var queueOption = this._selectNode.firstChild;
+
+    while (queueOption && queueOption.value != testName) {
+        queueOption = queueOption.nextSibling;
+    }
+
+    if (!queueOption)
+        return;
+
+    this._selectNode.removeChild(queueOption);
+    var test = results.tests[testName];
+    test.state = STATE_NEEDS_REBASELINE;
+    updateState();
+};
+
+RebaselineQueue.prototype.rebaseline = function()
+{
+    var testNames = [];
+    for (var queueOption = this._selectNode.firstChild;
+         queueOption;
+         queueOption = queueOption.nextSibling) {
+        testNames.push(queueOption.value);
+    }
+
+    this._inProgressRebaselineCount = testNames.length;
+    updateState();
+
+    testNames.forEach(this._rebaselineTest, this);
+};
+
+RebaselineQueue.prototype._rebaselineTest = function(testName)
+{
+    var baselineTarget = getSelectValue('baseline-target');
+    var baselineMoveTo = getSelectValue('baseline-move-to');
+
+    var xhr = new XMLHttpRequest();
+    xhr.open('POST',
+        '/rebaseline?test=' + encodeURIComponent(testName) +
+        '&baseline-target=' + encodeURIComponent(baselineTarget) +
+        '&baseline-move-to=' + encodeURIComponent(baselineMoveTo));
+
+    var self = this;
+    function handleResponse(logType, newState) {
+        log(xhr.responseText, logType);
+        self._removeTest(testName);
+        self._inProgressRebaselineCount--;
+        results.tests[testName].state = newState;
+        updateState();
+        // If we're done with a set of rebaselines, regenerate the test menu
+        // (which is grouped by state) since test states have changed.
+        if (self._inProgressRebaselineCount == 0) {
+            selectDirectory();
+        }
+    }
+
+    function handleSuccess() {
+        handleResponse(log.SUCCESS, STATE_REBASELINE_SUCCEEDED);
+    }
+    function handleFailure() {
+        handleResponse(log.ERROR, STATE_REBASELINE_FAILED);
+    }
+
+    xhr.addEventListener('load', function() {
+      if (xhr.status < 400) {
+          handleSuccess();
+      } else {
+          handleFailure();
+      }
+    });
+    xhr.addEventListener('error', handleFailure);
+
+    xhr.send();
+};
diff --git a/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/util.js b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/util.js
new file mode 100644
index 0000000..5ad7612
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/util.js
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+
+var results;
+var testsByFailureType = {};
+var testsByDirectory = {};
+var selectedTests = [];
+
+function $(id)
+{
+    return document.getElementById(id);
+}
+
+function getSelectValue(id) 
+{
+    var select = $(id);
+    if (select.selectedIndex == -1) {
+        return null;
+    } else {
+        return select.options[select.selectedIndex].value;
+    }
+}
+
+function loadText(url, callback)
+{
+    var xhr = new XMLHttpRequest();
+    xhr.open('GET', url);
+    xhr.addEventListener('load', function() { callback(xhr.responseText); });
+    xhr.send();
+}
+
+function log(text, type)
+{
+    var node = $('log');
+    
+    if (type) {
+        var typeNode = document.createElement('span');
+        typeNode.textContent = type.text;
+        typeNode.style.color = type.color;
+        node.appendChild(typeNode);
+    }
+
+    node.appendChild(document.createTextNode(text + '\n'));
+    node.scrollTop = node.scrollHeight;
+}
+
+log.WARNING = {text: 'Warning: ', color: '#aa3'};
+log.SUCCESS = {text: 'Success: ', color: 'green'};
+log.ERROR = {text: 'Error: ', color: 'red'};
+
+function toggle(id)
+{
+    var element = $(id);
+    var toggler = $('toggle-' + id);
+    if (element.style.display == 'none') {
+        element.style.display = '';
+        toggler.className = 'link selected';
+    } else {
+        element.style.display = 'none';
+        toggler.className = 'link';
+    }
+}
+
+function getTracUrl(layoutTestPath)
+{
+  return 'http://trac.webkit.org/browser/trunk/LayoutTests/' + layoutTestPath;
+}
+
+function getSortedKeys(obj)
+{
+    var keys = [];
+    for (var key in obj) {
+        keys.push(key);
+    }
+    keys.sort();
+    return keys;
+}
\ No newline at end of file
diff --git a/Tools/Scripts/webkitpy/tool/servers/gardeningserver.py b/Tools/Scripts/webkitpy/tool/servers/gardeningserver.py
new file mode 100644
index 0000000..e89c2a0
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/servers/gardeningserver.py
@@ -0,0 +1,142 @@
+# Copyright (C) 2011 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:
+#
+# 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 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 BaseHTTPServer
+import SocketServer
+import logging
+import json
+import os
+import sys
+import urllib
+
+from webkitpy.common.memoized import memoized
+from webkitpy.tool.servers.reflectionhandler import ReflectionHandler
+from webkitpy.layout_tests.port import builders
+
+
+_log = logging.getLogger(__name__)
+
+
+class BuildCoverageExtrapolator(object):
+    def __init__(self, test_configuration_converter):
+        self._test_configuration_converter = test_configuration_converter
+
+    @memoized
+    def _covered_test_configurations_for_builder_name(self):
+        coverage = {}
+        for builder_name in builders.all_builder_names():
+            coverage[builder_name] = self._test_configuration_converter.to_config_set(builders.coverage_specifiers_for_builder_name(builder_name))
+        return coverage
+
+    def extrapolate_test_configurations(self, builder_name):
+        return self._covered_test_configurations_for_builder_name()[builder_name]
+
+
+class GardeningHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
+    def __init__(self, httpd_port, config):
+        server_name = ''
+        self.tool = config['tool']
+        self.options = config['options']
+        BaseHTTPServer.HTTPServer.__init__(self, (server_name, httpd_port), GardeningHTTPRequestHandler)
+
+    def url(self, args=None):
+        # We can't use urllib.encode() here because that encodes spaces as plus signs and the buildbots don't decode those properly.
+        arg_string = ('?' + '&'.join("%s=%s" % (key, urllib.quote(value)) for (key, value) in args.items())) if args else ''
+        return 'file://' + os.path.join(GardeningHTTPRequestHandler.STATIC_FILE_DIRECTORY, 'garden-o-matic.html' + arg_string)
+
+
+class GardeningHTTPRequestHandler(ReflectionHandler):
+    STATIC_FILE_NAMES = frozenset()
+
+    STATIC_FILE_DIRECTORY = os.path.join(
+        os.path.dirname(__file__),
+        '..',
+        '..',
+        '..',
+        '..',
+        'BuildSlaveSupport',
+        'build.webkit.org-config',
+        'public_html',
+        'TestFailures')
+
+    allow_cross_origin_requests = True
+    debug_output = ''
+
+    def rollout(self):
+        revision = self.query['revision'][0]
+        reason = self.query['reason'][0]
+        self._run_webkit_patch([
+            'rollout',
+            '--force-clean',
+            '--non-interactive',
+            revision,
+            reason,
+        ])
+        self._serve_text('success')
+
+    def ping(self):
+        self._serve_text('pong')
+
+    def _run_webkit_patch(self, command, input_string):
+        PIPE = self.server.tool.executive.PIPE
+        process = self.server.tool.executive.popen([self.server.tool.path()] + command, cwd=self.server.tool.scm().checkout_root, stdin=PIPE, stdout=PIPE, stderr=PIPE)
+        process.stdin.write(input_string)
+        output, error = process.communicate()
+        return (process.returncode, output, error)
+
+    def rebaselineall(self):
+        command = ['rebaseline-json']
+        if self.server.options.move_overwritten_baselines:
+            command.append('--move-overwritten-baselines')
+        if self.server.options.results_directory:
+            command.extend(['--results-directory', self.server.options.results_directory])
+        if not self.server.options.optimize:
+            command.append('--no-optimize')
+        if self.server.options.verbose:
+            command.append('--verbose')
+        json_input = self.read_entity_body()
+
+        _log.debug("calling %s, input='%s'", command, json_input)
+        return_code, output, error = self._run_webkit_patch(command, json_input)
+        print >> sys.stderr, error
+        if return_code:
+            _log.error("rebaseline-json failed: %d, output='%s'" % (return_code, output))
+        else:
+            _log.debug("rebaseline-json succeeded")
+
+        # FIXME: propagate error and/or log messages back to the UI.
+        self._serve_text('success')
+
+    def localresult(self):
+        path = self.query['path'][0]
+        filesystem = self.server.tool.filesystem
+
+        # Ensure that we're only serving files from inside the results directory.
+        if not filesystem.isabs(path) and self.server.options.results_directory:
+            fullpath = filesystem.abspath(filesystem.join(self.server.options.results_directory, path))
+            if fullpath.startswith(filesystem.abspath(self.server.options.results_directory)):
+                self._serve_file(fullpath, headers_only=(self.command == 'HEAD'))
+                return
+
+        self._send_response(403)
diff --git a/Tools/Scripts/webkitpy/tool/servers/gardeningserver_unittest.py b/Tools/Scripts/webkitpy/tool/servers/gardeningserver_unittest.py
new file mode 100644
index 0000000..9961648
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/servers/gardeningserver_unittest.py
@@ -0,0 +1,123 @@
+# Copyright (C) 2011 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 json
+import sys
+import unittest
+
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.layout_tests.models.test_configuration import *
+from webkitpy.layout_tests.port import builders
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.mocktool import MockTool
+from webkitpy.common.system.executive_mock import MockExecutive
+from webkitpy.common.host_mock import MockHost
+from webkitpy.tool.servers.gardeningserver import *
+
+
+class TestPortFactory(object):
+    # FIXME: Why is this a class method?
+    @classmethod
+    def create(cls):
+        host = MockHost()
+        return host.port_factory.get("test-win-xp")
+
+    @classmethod
+    def path_to_test_expectations_file(cls):
+        return cls.create().path_to_test_expectations_file()
+
+
+class MockServer(object):
+    def __init__(self):
+        self.tool = MockTool()
+        self.tool.executive = MockExecutive(should_log=True)
+        self.tool.filesystem.files[TestPortFactory.path_to_test_expectations_file()] = ""
+
+
+# The real GardeningHTTPRequestHandler has a constructor that's too hard to
+# call in a unit test, so we create a subclass that's easier to constrcut.
+class TestGardeningHTTPRequestHandler(GardeningHTTPRequestHandler):
+    def __init__(self, server):
+        self.server = server
+        self.body = None
+
+    def _expectations_updater(self):
+        return GardeningExpectationsUpdater(self.server.tool, TestPortFactory.create())
+
+    def read_entity_body(self):
+        return self.body if self.body else ''
+
+    def _serve_text(self, text):
+        print "== Begin Response =="
+        print text
+        print "== End Response =="
+
+    def _serve_json(self, json_object):
+        print "== Begin JSON Response =="
+        print json.dumps(json_object)
+        print "== End JSON Response =="
+
+
+class BuildCoverageExtrapolatorTest(unittest.TestCase):
+    def test_extrapolate(self):
+        # FIXME: Make this test not rely on actual (not mock) port objects.
+        host = MockHost()
+        port = host.port_factory.get('chromium-win-win7', None)
+        converter = TestConfigurationConverter(port.all_test_configurations(), port.configuration_specifier_macros())
+        extrapolator = BuildCoverageExtrapolator(converter)
+        self.assertEquals(extrapolator.extrapolate_test_configurations("WebKit XP"), set([TestConfiguration(version='xp', architecture='x86', build_type='release')]))
+        self.assertRaises(KeyError, extrapolator.extrapolate_test_configurations, "Potato")
+
+
+class GardeningServerTest(unittest.TestCase):
+    def _post_to_path(self, path, body=None, expected_stderr=None, expected_stdout=None, server=None):
+        handler = TestGardeningHTTPRequestHandler(server or MockServer())
+        handler.path = path
+        handler.body = body
+        OutputCapture().assert_outputs(self, handler.do_POST, expected_stderr=expected_stderr, expected_stdout=expected_stdout)
+
+    def disabled_test_rollout(self):
+        expected_stderr = "MOCK run_command: ['echo', 'rollout', '--force-clean', '--non-interactive', '2314', 'MOCK rollout reason'], cwd=/mock-checkout\n"
+        expected_stdout = "== Begin Response ==\nsuccess\n== End Response ==\n"
+        self._post_to_path("/rollout?revision=2314&reason=MOCK+rollout+reason", expected_stderr=expected_stderr, expected_stdout=expected_stdout)
+
+    def disabled_test_rebaselineall(self):
+        expected_stderr = "MOCK run_command: ['echo', 'rebaseline-json'], cwd=/mock-checkout, input={\"user-scripts/another-test.html\":{\"%s\": [%s]}}\n"
+        expected_stdout = "== Begin Response ==\nsuccess\n== End Response ==\n"
+        server = MockServer()
+
+        self.output = ['{"add": [], "delete": []}', '']
+
+        def run_command(args, cwd=None, input=None, **kwargs):
+            print >> sys.stderr, "MOCK run_command: %s, cwd=%s, input=%s" % (args, cwd, input)
+            return self.output.pop(0)
+
+        server.tool.executive.run_command = run_command
+        self._post_to_path("/rebaselineall", body='{"user-scripts/another-test.html":{"MOCK builder": ["txt","png"]}}', expected_stderr=expected_stderr % ('MOCK builder', '"txt","png"'), expected_stdout=expected_stdout, server=server)
+
+        self._post_to_path("/rebaselineall", body='{"user-scripts/another-test.html":{"MOCK builder (Debug)": ["txt","png"]}}', expected_stderr=expected_stderr % ('MOCK builder (Debug)', '"txt","png"'), expected_stdout=expected_stdout)
diff --git a/Tools/Scripts/webkitpy/tool/servers/rebaselineserver.py b/Tools/Scripts/webkitpy/tool/servers/rebaselineserver.py
new file mode 100644
index 0000000..9e9c379
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/servers/rebaselineserver.py
@@ -0,0 +1,288 @@
+# 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 fnmatch
+import os
+import os.path
+import BaseHTTPServer
+
+from webkitpy.common.host import Host  # FIXME: This should not be needed!
+from webkitpy.layout_tests.port.base import Port
+from webkitpy.tool.servers.reflectionhandler import ReflectionHandler
+
+
+STATE_NEEDS_REBASELINE = 'needs_rebaseline'
+STATE_REBASELINE_FAILED = 'rebaseline_failed'
+STATE_REBASELINE_SUCCEEDED = 'rebaseline_succeeded'
+
+
+def _get_actual_result_files(test_file, test_config):
+    test_name, _ = os.path.splitext(test_file)
+    test_directory = os.path.dirname(test_file)
+
+    test_results_directory = test_config.filesystem.join(
+        test_config.results_directory, test_directory)
+    actual_pattern = os.path.basename(test_name) + '-actual.*'
+    actual_files = []
+    for filename in test_config.filesystem.listdir(test_results_directory):
+        if fnmatch.fnmatch(filename, actual_pattern):
+            actual_files.append(filename)
+    actual_files.sort()
+    return tuple(actual_files)
+
+
+def _rebaseline_test(test_file, baseline_target, baseline_move_to, test_config, log):
+    test_name, _ = os.path.splitext(test_file)
+    test_directory = os.path.dirname(test_name)
+
+    log('Rebaselining %s...' % test_name)
+
+    actual_result_files = _get_actual_result_files(test_file, test_config)
+    filesystem = test_config.filesystem
+    scm = test_config.scm
+    layout_tests_directory = test_config.layout_tests_directory
+    results_directory = test_config.results_directory
+    target_expectations_directory = filesystem.join(
+        layout_tests_directory, 'platform', baseline_target, test_directory)
+    test_results_directory = test_config.filesystem.join(
+        test_config.results_directory, test_directory)
+
+    # If requested, move current baselines out
+    current_baselines = get_test_baselines(test_file, test_config)
+    if baseline_target in current_baselines and baseline_move_to != 'none':
+        log('  Moving current %s baselines to %s' %
+            (baseline_target, baseline_move_to))
+
+        # See which ones we need to move (only those that are about to be
+        # updated), and make sure we're not clobbering any files in the
+        # destination.
+        current_extensions = set(current_baselines[baseline_target].keys())
+        actual_result_extensions = [
+            os.path.splitext(f)[1] for f in actual_result_files]
+        extensions_to_move = current_extensions.intersection(
+            actual_result_extensions)
+
+        if extensions_to_move.intersection(
+            current_baselines.get(baseline_move_to, {}).keys()):
+            log('    Already had baselines in %s, could not move existing '
+                '%s ones' % (baseline_move_to, baseline_target))
+            return False
+
+        # Do the actual move.
+        if extensions_to_move:
+            if not _move_test_baselines(
+                test_file,
+                list(extensions_to_move),
+                baseline_target,
+                baseline_move_to,
+                test_config,
+                log):
+                return False
+        else:
+            log('    No current baselines to move')
+
+    log('  Updating baselines for %s' % baseline_target)
+    filesystem.maybe_make_directory(target_expectations_directory)
+    for source_file in actual_result_files:
+        source_path = filesystem.join(test_results_directory, source_file)
+        destination_file = source_file.replace('-actual', '-expected')
+        destination_path = filesystem.join(
+            target_expectations_directory, destination_file)
+        filesystem.copyfile(source_path, destination_path)
+        exit_code = scm.add(destination_path, return_exit_code=True)
+        if exit_code:
+            log('    Could not update %s in SCM, exit code %d' %
+                (destination_file, exit_code))
+            return False
+        else:
+            log('    Updated %s' % destination_file)
+
+    return True
+
+
+def _move_test_baselines(test_file, extensions_to_move, source_platform, destination_platform, test_config, log):
+    test_file_name = os.path.splitext(os.path.basename(test_file))[0]
+    test_directory = os.path.dirname(test_file)
+    filesystem = test_config.filesystem
+
+    # Want predictable output order for unit tests.
+    extensions_to_move.sort()
+
+    source_directory = os.path.join(
+        test_config.layout_tests_directory,
+        'platform',
+        source_platform,
+        test_directory)
+    destination_directory = os.path.join(
+        test_config.layout_tests_directory,
+        'platform',
+        destination_platform,
+        test_directory)
+    filesystem.maybe_make_directory(destination_directory)
+
+    for extension in extensions_to_move:
+        file_name = test_file_name + '-expected' + extension
+        source_path = filesystem.join(source_directory, file_name)
+        destination_path = filesystem.join(destination_directory, file_name)
+        filesystem.copyfile(source_path, destination_path)
+        exit_code = test_config.scm.add(destination_path, return_exit_code=True)
+        if exit_code:
+            log('    Could not update %s in SCM, exit code %d' %
+                (file_name, exit_code))
+            return False
+        else:
+            log('    Moved %s' % file_name)
+
+    return True
+
+
+def get_test_baselines(test_file, test_config):
+    # FIXME: This seems like a hack. This only seems used to access the Port.expected_baselines logic.
+    class AllPlatformsPort(Port):
+        def __init__(self, host):
+            super(AllPlatformsPort, self).__init__(host, 'mac')
+            self._platforms_by_directory = dict([(self._webkit_baseline_path(p), p) for p in test_config.platforms])
+
+        def baseline_search_path(self):
+            return self._platforms_by_directory.keys()
+
+        def platform_from_directory(self, directory):
+            return self._platforms_by_directory[directory]
+
+    test_path = test_config.filesystem.join(test_config.layout_tests_directory, test_file)
+
+    # FIXME: This should get the Host from the test_config to be mockable!
+    host = Host()
+    host.initialize_scm()
+    host.filesystem = test_config.filesystem
+    all_platforms_port = AllPlatformsPort(host)
+
+    all_test_baselines = {}
+    for baseline_extension in ('.txt', '.checksum', '.png'):
+        test_baselines = test_config.test_port.expected_baselines(test_file, baseline_extension)
+        baselines = all_platforms_port.expected_baselines(test_file, baseline_extension, all_baselines=True)
+        for platform_directory, expected_filename in baselines:
+            if not platform_directory:
+                continue
+            if platform_directory == test_config.layout_tests_directory:
+                platform = 'base'
+            else:
+                platform = all_platforms_port.platform_from_directory(platform_directory)
+            platform_baselines = all_test_baselines.setdefault(platform, {})
+            was_used_for_test = (platform_directory, expected_filename) in test_baselines
+            platform_baselines[baseline_extension] = was_used_for_test
+
+    return all_test_baselines
+
+
+class RebaselineHTTPServer(BaseHTTPServer.HTTPServer):
+    def __init__(self, httpd_port, config):
+        server_name = ""
+        BaseHTTPServer.HTTPServer.__init__(self, (server_name, httpd_port), RebaselineHTTPRequestHandler)
+        self.test_config = config['test_config']
+        self.results_json = config['results_json']
+        self.platforms_json = config['platforms_json']
+
+
+class RebaselineHTTPRequestHandler(ReflectionHandler):
+    STATIC_FILE_NAMES = frozenset([
+        "index.html",
+        "loupe.js",
+        "main.js",
+        "main.css",
+        "queue.js",
+        "util.js",
+    ])
+
+    STATIC_FILE_DIRECTORY = os.path.join(os.path.dirname(__file__), "data", "rebaselineserver")
+
+    def results_json(self):
+        self._serve_json(self.server.results_json)
+
+    def test_config(self):
+        self._serve_json(self.server.test_config)
+
+    def platforms_json(self):
+        self._serve_json(self.server.platforms_json)
+
+    def rebaseline(self):
+        test = self.query['test'][0]
+        baseline_target = self.query['baseline-target'][0]
+        baseline_move_to = self.query['baseline-move-to'][0]
+        test_json = self.server.results_json['tests'][test]
+
+        if test_json['state'] != STATE_NEEDS_REBASELINE:
+            self.send_error(400, "Test %s is in unexpected state: %s" % (test, test_json["state"]))
+            return
+
+        log = []
+        success = _rebaseline_test(
+            test,
+            baseline_target,
+            baseline_move_to,
+            self.server.test_config,
+            log=lambda l: log.append(l))
+
+        if success:
+            test_json['state'] = STATE_REBASELINE_SUCCEEDED
+            self.send_response(200)
+        else:
+            test_json['state'] = STATE_REBASELINE_FAILED
+            self.send_response(500)
+
+        self.send_header('Content-type', 'text/plain')
+        self.end_headers()
+        self.wfile.write('\n'.join(log))
+
+    def test_result(self):
+        test_name, _ = os.path.splitext(self.query['test'][0])
+        mode = self.query['mode'][0]
+        if mode == 'expected-image':
+            file_name = test_name + '-expected.png'
+        elif mode == 'actual-image':
+            file_name = test_name + '-actual.png'
+        if mode == 'expected-checksum':
+            file_name = test_name + '-expected.checksum'
+        elif mode == 'actual-checksum':
+            file_name = test_name + '-actual.checksum'
+        elif mode == 'diff-image':
+            file_name = test_name + '-diff.png'
+        if mode == 'expected-text':
+            file_name = test_name + '-expected.txt'
+        elif mode == 'actual-text':
+            file_name = test_name + '-actual.txt'
+        elif mode == 'diff-text':
+            file_name = test_name + '-diff.txt'
+        elif mode == 'diff-text-pretty':
+            file_name = test_name + '-pretty-diff.html'
+
+        file_path = os.path.join(self.server.test_config.results_directory, file_name)
+
+        # Let results be cached for 60 seconds, so that they can be pre-fetched
+        # by the UI
+        self._serve_file(file_path, cacheable_seconds=60)
diff --git a/Tools/Scripts/webkitpy/tool/servers/rebaselineserver_unittest.py b/Tools/Scripts/webkitpy/tool/servers/rebaselineserver_unittest.py
new file mode 100644
index 0000000..f5c1cbf
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/servers/rebaselineserver_unittest.py
@@ -0,0 +1,311 @@
+# 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 json
+import unittest
+
+from webkitpy.common.net import resultsjsonparser_unittest
+from webkitpy.common.host_mock import MockHost
+from webkitpy.layout_tests.layout_package.json_results_generator import strip_json_wrapper
+from webkitpy.layout_tests.port.base import Port
+from webkitpy.tool.commands.rebaselineserver import TestConfig, RebaselineServer
+from webkitpy.tool.servers import rebaselineserver
+
+
+class RebaselineTestTest(unittest.TestCase):
+    def test_text_rebaseline_update(self):
+        self._assertRebaseline(
+            test_files=(
+                'fast/text-expected.txt',
+                'platform/mac/fast/text-expected.txt',
+            ),
+            results_files=(
+                'fast/text-actual.txt',
+            ),
+            test_name='fast/text.html',
+            baseline_target='mac',
+            baseline_move_to='none',
+            expected_success=True,
+            expected_log=[
+                'Rebaselining fast/text...',
+                '  Updating baselines for mac',
+                '    Updated text-expected.txt',
+            ])
+
+    def test_text_rebaseline_new(self):
+        self._assertRebaseline(
+            test_files=(
+                'fast/text-expected.txt',
+            ),
+            results_files=(
+                'fast/text-actual.txt',
+            ),
+            test_name='fast/text.html',
+            baseline_target='mac',
+            baseline_move_to='none',
+            expected_success=True,
+            expected_log=[
+                'Rebaselining fast/text...',
+                '  Updating baselines for mac',
+                '    Updated text-expected.txt',
+            ])
+
+    def test_text_rebaseline_move_no_op_1(self):
+        self._assertRebaseline(
+            test_files=(
+                'fast/text-expected.txt',
+                'platform/win/fast/text-expected.txt',
+            ),
+            results_files=(
+                'fast/text-actual.txt',
+            ),
+            test_name='fast/text.html',
+            baseline_target='mac',
+            baseline_move_to='mac-leopard',
+            expected_success=True,
+            expected_log=[
+                'Rebaselining fast/text...',
+                '  Updating baselines for mac',
+                '    Updated text-expected.txt',
+            ])
+
+    def test_text_rebaseline_move_no_op_2(self):
+        self._assertRebaseline(
+            test_files=(
+                'fast/text-expected.txt',
+                'platform/mac/fast/text-expected.checksum',
+            ),
+            results_files=(
+                'fast/text-actual.txt',
+            ),
+            test_name='fast/text.html',
+            baseline_target='mac',
+            baseline_move_to='mac-leopard',
+            expected_success=True,
+            expected_log=[
+                'Rebaselining fast/text...',
+                '  Moving current mac baselines to mac-leopard',
+                '    No current baselines to move',
+                '  Updating baselines for mac',
+                '    Updated text-expected.txt',
+            ])
+
+    def test_text_rebaseline_move(self):
+        self._assertRebaseline(
+            test_files=(
+                'fast/text-expected.txt',
+                'platform/mac/fast/text-expected.txt',
+            ),
+            results_files=(
+                'fast/text-actual.txt',
+            ),
+            test_name='fast/text.html',
+            baseline_target='mac',
+            baseline_move_to='mac-leopard',
+            expected_success=True,
+            expected_log=[
+                'Rebaselining fast/text...',
+                '  Moving current mac baselines to mac-leopard',
+                '    Moved text-expected.txt',
+                '  Updating baselines for mac',
+                '    Updated text-expected.txt',
+            ])
+
+    def test_text_rebaseline_move_only_images(self):
+        self._assertRebaseline(
+            test_files=(
+                'fast/image-expected.txt',
+                'platform/mac/fast/image-expected.txt',
+                'platform/mac/fast/image-expected.png',
+                'platform/mac/fast/image-expected.checksum',
+            ),
+            results_files=(
+                'fast/image-actual.png',
+                'fast/image-actual.checksum',
+            ),
+            test_name='fast/image.html',
+            baseline_target='mac',
+            baseline_move_to='mac-leopard',
+            expected_success=True,
+            expected_log=[
+                'Rebaselining fast/image...',
+                '  Moving current mac baselines to mac-leopard',
+                '    Moved image-expected.checksum',
+                '    Moved image-expected.png',
+                '  Updating baselines for mac',
+                '    Updated image-expected.checksum',
+                '    Updated image-expected.png',
+            ])
+
+    def test_text_rebaseline_move_already_exist(self):
+        self._assertRebaseline(
+            test_files=(
+                'fast/text-expected.txt',
+                'platform/mac-leopard/fast/text-expected.txt',
+                'platform/mac/fast/text-expected.txt',
+            ),
+            results_files=(
+                'fast/text-actual.txt',
+            ),
+            test_name='fast/text.html',
+            baseline_target='mac',
+            baseline_move_to='mac-leopard',
+            expected_success=False,
+            expected_log=[
+                'Rebaselining fast/text...',
+                '  Moving current mac baselines to mac-leopard',
+                '    Already had baselines in mac-leopard, could not move existing mac ones',
+            ])
+
+    def test_image_rebaseline(self):
+        self._assertRebaseline(
+            test_files=(
+                'fast/image-expected.txt',
+                'platform/mac/fast/image-expected.png',
+                'platform/mac/fast/image-expected.checksum',
+            ),
+            results_files=(
+                'fast/image-actual.png',
+                'fast/image-actual.checksum',
+            ),
+            test_name='fast/image.html',
+            baseline_target='mac',
+            baseline_move_to='none',
+            expected_success=True,
+            expected_log=[
+                'Rebaselining fast/image...',
+                '  Updating baselines for mac',
+                '    Updated image-expected.checksum',
+                '    Updated image-expected.png',
+            ])
+
+    def test_gather_baselines(self):
+        example_json = resultsjsonparser_unittest.ResultsJSONParserTest._example_full_results_json
+        results_json = json.loads(strip_json_wrapper(example_json))
+        server = RebaselineServer()
+        server._test_config = get_test_config()
+        server._gather_baselines(results_json)
+        self.assertEqual(results_json['tests']['svg/dynamic-updates/SVGFEDropShadowElement-dom-stdDeviation-attr.html']['state'], 'needs_rebaseline')
+        self.assertFalse('prototype-chocolate.html' in results_json['tests'])
+
+    def _assertRebaseline(self, test_files, results_files, test_name, baseline_target, baseline_move_to, expected_success, expected_log):
+        log = []
+        test_config = get_test_config(test_files, results_files)
+        success = rebaselineserver._rebaseline_test(
+            test_name,
+            baseline_target,
+            baseline_move_to,
+            test_config,
+            log=lambda l: log.append(l))
+        self.assertEqual(expected_log, log)
+        self.assertEqual(expected_success, success)
+
+
+class GetActualResultFilesTest(unittest.TestCase):
+    def test(self):
+        test_config = get_test_config(result_files=(
+            'fast/text-actual.txt',
+            'fast2/text-actual.txt',
+            'fast/text2-actual.txt',
+            'fast/text-notactual.txt',
+        ))
+        self.assertEqual(
+            ('text-actual.txt',),
+            rebaselineserver._get_actual_result_files(
+                'fast/text.html', test_config))
+
+
+class GetBaselinesTest(unittest.TestCase):
+    def test_no_baselines(self):
+        self._assertBaselines(
+            test_files=(),
+            test_name='fast/missing.html',
+            expected_baselines={})
+
+    def test_text_baselines(self):
+        self._assertBaselines(
+            test_files=(
+                'fast/text-expected.txt',
+                'platform/mac/fast/text-expected.txt',
+            ),
+            test_name='fast/text.html',
+            expected_baselines={
+                'mac': {'.txt': True},
+                'base': {'.txt': False},
+            })
+
+    def test_image_and_text_baselines(self):
+        self._assertBaselines(
+            test_files=(
+                'fast/image-expected.txt',
+                'platform/mac/fast/image-expected.png',
+                'platform/mac/fast/image-expected.checksum',
+                'platform/win/fast/image-expected.png',
+                'platform/win/fast/image-expected.checksum',
+            ),
+            test_name='fast/image.html',
+            expected_baselines={
+                'base': {'.txt': True},
+                'mac': {'.checksum': True, '.png': True},
+                'win': {'.checksum': False, '.png': False},
+            })
+
+    def test_extra_baselines(self):
+        self._assertBaselines(
+            test_files=(
+                'fast/text-expected.txt',
+                'platform/nosuchplatform/fast/text-expected.txt',
+            ),
+            test_name='fast/text.html',
+            expected_baselines={'base': {'.txt': True}})
+
+    def _assertBaselines(self, test_files, test_name, expected_baselines):
+        actual_baselines = rebaselineserver.get_test_baselines(test_name, get_test_config(test_files))
+        self.assertEqual(expected_baselines, actual_baselines)
+
+
+def get_test_config(test_files=[], result_files=[]):
+    # We could grab this from port.layout_tests_dir(), but instantiating a fully mocked port is a pain.
+    layout_tests_directory = "/mock-checkout/LayoutTests"
+    results_directory = '/WebKitBuild/Debug/layout-test-results'
+    host = MockHost()
+    for file in test_files:
+        host.filesystem.write_binary_file(host.filesystem.join(layout_tests_directory, file), '')
+    for file in result_files:
+        host.filesystem.write_binary_file(host.filesystem.join(results_directory, file), '')
+
+    class TestMacPort(Port):
+        port_name = "mac"
+
+    return TestConfig(
+        TestMacPort(host, 'mac'),
+        layout_tests_directory,
+        results_directory,
+        ('mac', 'mac-leopard', 'win', 'linux'),
+        host.filesystem,
+        host.scm())
diff --git a/Tools/Scripts/webkitpy/tool/servers/reflectionhandler.py b/Tools/Scripts/webkitpy/tool/servers/reflectionhandler.py
new file mode 100644
index 0000000..9308709
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/servers/reflectionhandler.py
@@ -0,0 +1,143 @@
+# Copyright (c) 2011 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 BaseHTTPServer
+
+import cgi
+import codecs
+import datetime
+import fnmatch
+import json
+import mimetypes
+import os.path
+import shutil
+import threading
+import time
+import urlparse
+import wsgiref.handlers
+import BaseHTTPServer
+
+
+class ReflectionHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+    # Subclasses should override.
+    STATIC_FILE_NAMES = None
+    STATIC_FILE_DIRECTORY = None
+
+    # Setting this flag to True causes the server to send
+    #   Access-Control-Allow-Origin: *
+    # with every response.
+    allow_cross_origin_requests = False
+
+    def do_GET(self):
+        self._handle_request()
+
+    def do_POST(self):
+        self._handle_request()
+
+    def do_HEAD(self):
+        self._handle_request()
+
+    def read_entity_body(self):
+        length = int(self.headers.getheader('content-length'))
+        return self.rfile.read(length)
+
+    def _read_entity_body_as_json(self):
+        return json.loads(self.read_entity_body())
+
+    def _handle_request(self):
+        if "?" in self.path:
+            path, query_string = self.path.split("?", 1)
+            self.query = cgi.parse_qs(query_string)
+        else:
+            path = self.path
+            self.query = {}
+        function_or_file_name = path[1:] or "index.html"
+
+        if function_or_file_name in self.STATIC_FILE_NAMES:
+            self._serve_static_file(function_or_file_name)
+            return
+
+        function_name = function_or_file_name.replace(".", "_")
+        if not hasattr(self, function_name):
+            self.send_error(404, "Unknown function %s" % function_name)
+            return
+        if function_name[0] == "_":
+            self.send_error(401, "Not allowed to invoke private or protected methods")
+            return
+        function = getattr(self, function_name)
+        function()
+
+    def _serve_static_file(self, static_path):
+        self._serve_file(os.path.join(self.STATIC_FILE_DIRECTORY, static_path))
+
+    def quitquitquit(self):
+        self._serve_text("Server quit.\n")
+        # Shutdown has to happen on another thread from the server's thread,
+        # otherwise there's a deadlock
+        threading.Thread(target=lambda: self.server.shutdown()).start()
+
+    def _send_access_control_header(self):
+        if self.allow_cross_origin_requests:
+            self.send_header('Access-Control-Allow-Origin', '*')
+
+    def _serve_text(self, text):
+        self.send_response(200)
+        self._send_access_control_header()
+        self.send_header("Content-type", "text/plain")
+        self.end_headers()
+        self.wfile.write(text)
+
+    def _serve_json(self, json_object):
+        self.send_response(200)
+        self._send_access_control_header()
+        self.send_header('Content-type', 'application/json')
+        self.end_headers()
+        json.dump(json_object, self.wfile)
+
+    def _serve_file(self, file_path, cacheable_seconds=0, headers_only=False):
+        if not os.path.exists(file_path):
+            self.send_error(404, "File not found")
+            return
+        with codecs.open(file_path, "rb") as static_file:
+            self.send_response(200)
+            self._send_access_control_header()
+            self.send_header("Content-Length", os.path.getsize(file_path))
+            mime_type, encoding = mimetypes.guess_type(file_path)
+            if mime_type:
+                self.send_header("Content-type", mime_type)
+
+            if cacheable_seconds:
+                expires_time = (datetime.datetime.now() +
+                    datetime.timedelta(0, cacheable_seconds))
+                expires_formatted = wsgiref.handlers.format_date_time(
+                    time.mktime(expires_time.timetuple()))
+                self.send_header("Expires", expires_formatted)
+            self.end_headers()
+
+            if not headers_only:
+                shutil.copyfileobj(static_file, self.wfile)
diff --git a/Tools/Scripts/webkitpy/tool/steps/__init__.py b/Tools/Scripts/webkitpy/tool/steps/__init__.py
new file mode 100644
index 0000000..56429e8
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/__init__.py
@@ -0,0 +1,65 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# FIXME: Is this the right way to do this?
+from webkitpy.tool.steps.addsvnmimetypeforpng import AddSvnMimetypeForPng
+from webkitpy.tool.steps.applypatch import ApplyPatch
+from webkitpy.tool.steps.applypatchwithlocalcommit import ApplyPatchWithLocalCommit
+from webkitpy.tool.steps.applywatchlist import ApplyWatchList
+from webkitpy.tool.steps.attachtobug import AttachToBug
+from webkitpy.tool.steps.build import Build
+from webkitpy.tool.steps.checkstyle import CheckStyle
+from webkitpy.tool.steps.cleanworkingdirectory import CleanWorkingDirectory
+from webkitpy.tool.steps.cleanworkingdirectorywithlocalcommits import CleanWorkingDirectoryWithLocalCommits
+from webkitpy.tool.steps.closebug import CloseBug
+from webkitpy.tool.steps.closebugforlanddiff import CloseBugForLandDiff
+from webkitpy.tool.steps.closepatch import ClosePatch
+from webkitpy.tool.steps.commit import Commit
+from webkitpy.tool.steps.confirmdiff import ConfirmDiff
+from webkitpy.tool.steps.createbug import CreateBug
+from webkitpy.tool.steps.editchangelog import EditChangeLog
+from webkitpy.tool.steps.ensurebugisopenandassigned import EnsureBugIsOpenAndAssigned
+from webkitpy.tool.steps.ensurelocalcommitifneeded import EnsureLocalCommitIfNeeded
+from webkitpy.tool.steps.obsoletepatches import ObsoletePatches
+from webkitpy.tool.steps.options import Options
+from webkitpy.tool.steps.postdiff import PostDiff
+from webkitpy.tool.steps.postdiffforcommit import PostDiffForCommit
+from webkitpy.tool.steps.postdiffforrevert import PostDiffForRevert
+from webkitpy.tool.steps.preparechangelog import PrepareChangeLog
+from webkitpy.tool.steps.preparechangelogfordepsroll import PrepareChangeLogForDEPSRoll
+from webkitpy.tool.steps.preparechangelogforrevert import PrepareChangeLogForRevert
+from webkitpy.tool.steps.promptforbugortitle import PromptForBugOrTitle
+from webkitpy.tool.steps.reopenbugafterrollout import ReopenBugAfterRollout
+from webkitpy.tool.steps.revertrevision import RevertRevision
+from webkitpy.tool.steps.runtests import RunTests
+from webkitpy.tool.steps.suggestreviewers import SuggestReviewers
+from webkitpy.tool.steps.update import Update
+from webkitpy.tool.steps.updatechangelogswithreviewer import UpdateChangeLogsWithReviewer
+from webkitpy.tool.steps.updatechromiumdeps import UpdateChromiumDEPS
+from webkitpy.tool.steps.validatechangelogs import ValidateChangeLogs
+from webkitpy.tool.steps.validatereviewer import ValidateReviewer
diff --git a/Tools/Scripts/webkitpy/tool/steps/abstractstep.py b/Tools/Scripts/webkitpy/tool/steps/abstractstep.py
new file mode 100644
index 0000000..2a5fea6
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/abstractstep.py
@@ -0,0 +1,78 @@
+# 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 sys
+
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.tool.steps.options import Options
+
+
+class AbstractStep(object):
+    def __init__(self, tool, options):
+        self._tool = tool
+        self._options = options
+
+    def _exit(self, code):
+        sys.exit(code)
+
+    def _changed_files(self, state):
+        return self.cached_lookup(state, "changed_files")
+
+    _well_known_keys = {
+        # FIXME: Should this use state.get('bug_id') or state.get('patch').bug_id() like UpdateChangeLogsWithReviewer does?
+        "bug": lambda self, state: self._tool.bugs.fetch_bug(state["bug_id"]),
+        # bug_title can either be a new title given by the user, or one from an existing bug.
+        "bug_title": lambda self, state: self.cached_lookup(state, 'bug').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)),
+        # Absolute path to ChangeLog files.
+        "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):
+        if state.get(key):
+            return state[key]
+        if not promise:
+            promise = self._well_known_keys.get(key)
+        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 [
+            # We need this option here because cached_lookup uses it.  :(
+            Options.git_commit,
+        ]
+
+    def run(self, state):
+        raise NotImplementedError, "subclasses must implement"
diff --git a/Tools/Scripts/webkitpy/tool/steps/addsvnmimetypeforpng.py b/Tools/Scripts/webkitpy/tool/steps/addsvnmimetypeforpng.py
new file mode 100644
index 0000000..73bec15
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/addsvnmimetypeforpng.py
@@ -0,0 +1,77 @@
+# Copyright (C) 2012 Balazs Ankes (bank@inf.u-szeged.hu) University of Szeged
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.common import checksvnconfigfile
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.common.checkout.scm.detection import SCMDetector
+from webkitpy.common.system.systemhost import SystemHost
+
+
+class AddSvnMimetypeForPng(AbstractStep):
+    def __init__(self, tool, options, host=None, scm=None):
+        self._tool = tool
+        self._options = options
+        self._host = host or SystemHost()
+        self._fs = self._host.filesystem
+        self._detector = scm or SCMDetector(self._fs, self._host.executive).detect_scm_system(self._fs.getcwd())
+
+    def run(self, state):
+        png_files = self._check_pngs(self._changed_files(state))
+
+        if png_files:
+            detection = self._detector.display_name()
+
+            if detection == "git":
+                (file_missing, autoprop_missing, png_missing) = checksvnconfigfile.check(self._host, self._fs)
+                config_file_path = checksvnconfigfile.config_file_path(self._host, self._fs)
+
+                if file_missing:
+                    log("There is no SVN config file. The svn:mime-type of pngs won't set.")
+                    if not self._tool.user.confirm("Are you sure you want to continue?", default="n"):
+                        self._exit(1)
+                elif autoprop_missing and png_missing:
+                    log(checksvnconfigfile.errorstr_autoprop(config_file_path) + checksvnconfigfile.errorstr_png(config_file_path))
+                    if not self._tool.user.confirm("Do you want to continue?", default="n"):
+                        self._exit(1)
+                elif autoprop_missing:
+                    log(checksvnconfigfile.errorstr_autoprop(config_file_path))
+                    if not self._tool.user.confirm("Do you want to continue?", default="n"):
+                        self._exit(1)
+                elif png_missing:
+                    log(checksvnconfigfile.errorstr_png(config_file_path))
+                    if not self._tool.user.confirm("Do you want to continue?", default="n"):
+                        self._exit(1)
+
+            elif detection == "svn":
+                for filename in png_files:
+                    if self._detector.exists(filename) and self._detector.propget('svn:mime-type', filename) != 'image/png':
+                        print "Adding image/png mime-type to %s" % filename
+                        self._detector.propset('svn:mime-type', 'image/png', filename)
+
+    def _check_pngs(self, changed_files):
+        png_files = []
+        for filename in changed_files:
+            if filename.endswith('.png'):
+                png_files.append(filename)
+        return png_files
diff --git a/Tools/Scripts/webkitpy/tool/steps/addsvnmimetypeforpng_unittest.py b/Tools/Scripts/webkitpy/tool/steps/addsvnmimetypeforpng_unittest.py
new file mode 100644
index 0000000..221c6bc
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/addsvnmimetypeforpng_unittest.py
@@ -0,0 +1,58 @@
+# Copyright (C) 2012 Balazs Ankes (bank@inf.u-szeged.hu) University of Szeged
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#
+# 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.tool.steps.addsvnmimetypeforpng import AddSvnMimetypeForPng
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.tool.mocktool import MockOptions, MockTool
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+from webkitpy.common.system.outputcapture import OutputCapture
+
+
+class MockSCMDetector(object):
+
+    def __init__(self, scm):
+        self._scm = scm
+
+    def display_name(self):
+        return self._scm
+
+
+class AddSvnMimetypeForPngTest(unittest.TestCase):
+    def test_run(self):
+        capture = OutputCapture()
+        options = MockOptions(git_commit='MOCK git commit')
+
+        files = {'/Users/mock/.subversion/config': 'enable-auto-props = yes\n*.png = svn:mime-type=image/png'}
+        fs = MockFileSystem(files)
+        scm = MockSCMDetector('git')
+
+        step = AddSvnMimetypeForPng(MockTool(), options, MockSystemHost(os_name='linux', filesystem=fs), scm)
+        state = {
+            "changed_files": ["test.png", "test.txt"],
+        }
+        try:
+            capture.assert_outputs(self, step.run, [state])
+        except SystemExit, e:
+            self.assertEquals(e.code, 1)
diff --git a/Tools/Scripts/webkitpy/tool/steps/applypatch.py b/Tools/Scripts/webkitpy/tool/steps/applypatch.py
new file mode 100644
index 0000000..5c36169
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/applypatch.py
@@ -0,0 +1,42 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+from webkitpy.common.system.deprecated_logging import log
+
+class ApplyPatch(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.non_interactive,
+        ]
+
+    def run(self, state):
+        log("Processing patch %s from bug %s." % (state["patch"].id(), state["patch"].bug_id()))
+        self._tool.checkout().apply_patch(state["patch"])
diff --git a/Tools/Scripts/webkitpy/tool/steps/applypatchwithlocalcommit.py b/Tools/Scripts/webkitpy/tool/steps/applypatchwithlocalcommit.py
new file mode 100644
index 0000000..3dcd8d9
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/applypatchwithlocalcommit.py
@@ -0,0 +1,43 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.applypatch import ApplyPatch
+from webkitpy.tool.steps.options import Options
+
+class ApplyPatchWithLocalCommit(ApplyPatch):
+    @classmethod
+    def options(cls):
+        return ApplyPatch.options() + [
+            Options.local_commit,
+        ]
+
+    def run(self, state):
+        ApplyPatch.run(self, state)
+        if self._options.local_commit:
+            commit_message = self._tool.checkout().commit_message_for_this_commit(git_commit=None)
+            self._tool.scm().commit_locally_with_message(commit_message.message() or state["patch"].name())
diff --git a/Tools/Scripts/webkitpy/tool/steps/applywatchlist.py b/Tools/Scripts/webkitpy/tool/steps/applywatchlist.py
new file mode 100644
index 0000000..a4bb891
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/applywatchlist.py
@@ -0,0 +1,67 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.system import logutils
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+
+
+_log = logutils.get_logger(__file__)
+
+
+class ApplyWatchList(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.git_commit,
+        ]
+
+    def run(self, state):
+        diff = self.cached_lookup(state, 'diff')
+        bug_id = state.get('bug_id')
+
+        cc_and_messages = self._tool.watch_list().determine_cc_and_messages(diff)
+        cc_emails = cc_and_messages['cc_list']
+        messages = cc_and_messages['messages']
+        if bug_id:
+            # Remove emails and cc's which are already in the bug or the reporter.
+            bug = self._tool.bugs.fetch_bug(bug_id)
+
+            messages = filter(lambda message: not bug.is_in_comments(message), messages)
+            cc_emails = set(cc_emails).difference(bug.cc_emails())
+            cc_emails.discard(bug.reporter_email())
+
+        comment_text = '\n\n'.join(messages)
+        if bug_id:
+            if cc_emails or comment_text:
+                self._tool.bugs.post_comment_to_bug(bug_id, comment_text, cc_emails)
+            log_result = _log.debug
+        else:
+            _log.info('No bug was updated because no id was given.')
+            log_result = _log.info
+        log_result('Result of watchlist: cc "%s" messages "%s"' % (', '.join(cc_emails), comment_text))
diff --git a/Tools/Scripts/webkitpy/tool/steps/applywatchlist_unittest.py b/Tools/Scripts/webkitpy/tool/steps/applywatchlist_unittest.py
new file mode 100644
index 0000000..bdaaf75
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/applywatchlist_unittest.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2011 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.system.outputcapture import OutputCapture
+from webkitpy.tool.mocktool import MockOptions, MockTool
+from webkitpy.tool.steps.applywatchlist import ApplyWatchList
+
+
+class ApplyWatchListTest(unittest.TestCase):
+    def test_apply_watch_list_local(self):
+        capture = OutputCapture()
+        step = ApplyWatchList(MockTool(log_executive=True), MockOptions())
+        state = {
+            'bug_id': '50001',
+            'diff': 'The diff',
+        }
+        expected_stderr = """MockWatchList: determine_cc_and_messages
+MOCK bug comment: bug_id=50001, cc=set(['levin@chromium.org'])
+--- Begin comment ---
+Message2.
+--- End comment ---
+
+"""
+        capture.assert_outputs(self, step.run, [state], expected_stderr=expected_stderr)
diff --git a/Tools/Scripts/webkitpy/tool/steps/attachtobug.py b/Tools/Scripts/webkitpy/tool/steps/attachtobug.py
new file mode 100644
index 0000000..a389e65
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/attachtobug.py
@@ -0,0 +1,49 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+
+
+class AttachToBug(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.comment,
+            Options.description,
+        ]
+
+    def run(self, state):
+        filepath = state["filepath"]
+        bug_id = state["bug_id"]
+        description = self._options.description or self._tool.filesystem.basename(filepath)
+        comment_text = self._options.comment
+
+        # add_attachment_to_bug fills in the filename from the file path.
+        filename = None
+        self._tool.bugs.add_attachment_to_bug(bug_id, filepath, description, filename, comment_text)
diff --git a/Tools/Scripts/webkitpy/tool/steps/build.py b/Tools/Scripts/webkitpy/tool/steps/build.py
new file mode 100644
index 0000000..7f7dd9f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/build.py
@@ -0,0 +1,60 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+from webkitpy.common.system.deprecated_logging import log
+
+
+class Build(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.build,
+            Options.quiet,
+            Options.build_style,
+        ]
+
+    def build(self, build_style):
+        environment = self._tool.copy_current_environment()
+        environment.disable_gcc_smartquotes()
+        env = environment.to_dictionary()
+
+        build_webkit_command = self._tool.port().build_webkit_command(build_style=build_style)
+        self._tool.executive.run_and_throw_if_fail(build_webkit_command, self._options.quiet,
+            cwd=self._tool.scm().checkout_root, env=env)
+
+    def run(self, state):
+        if not self._options.build:
+            return
+        log("Building WebKit")
+        if self._options.build_style == "both":
+            self.build("debug")
+            self.build("release")
+        else:
+            self.build(self._options.build_style)
diff --git a/Tools/Scripts/webkitpy/tool/steps/checkstyle.py b/Tools/Scripts/webkitpy/tool/steps/checkstyle.py
new file mode 100644
index 0000000..3304f01
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/checkstyle.py
@@ -0,0 +1,68 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+from webkitpy.common.system.deprecated_logging import error
+
+class CheckStyle(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.non_interactive,
+            Options.check_style,
+            Options.check_style_filter,
+            Options.git_commit,
+        ]
+
+    def run(self, state):
+        if not self._options.check_style:
+            return
+
+        args = []
+        if self._options.git_commit:
+            args.append("--git-commit")
+            args.append(self._options.git_commit)
+
+        args.append("--diff-files")
+        args.extend(self._changed_files(state))
+
+        if self._options.check_style_filter:
+            args.append("--filter")
+            args.append(self._options.check_style_filter)
+
+        try:
+            self._tool.executive.run_and_throw_if_fail(self._tool.port().check_webkit_style_command() + args, cwd=self._tool.scm().checkout_root)
+        except ScriptError, e:
+            if self._options.non_interactive:
+                # We need to re-raise the exception here to have the
+                # style-queue do the right thing.
+                raise e
+            if not self._tool.user.confirm("Are you sure you want to continue?"):
+                self._exit(1)
diff --git a/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectory.py b/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectory.py
new file mode 100644
index 0000000..1913524
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectory.py
@@ -0,0 +1,50 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+
+
+class CleanWorkingDirectory(AbstractStep):
+    def __init__(self, tool, options, allow_local_commits=False):
+        AbstractStep.__init__(self, tool, options)
+        self._allow_local_commits = allow_local_commits
+
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.force_clean,
+            Options.clean,
+        ]
+
+    def run(self, state):
+        if not self._options.clean:
+            return
+        if not self._allow_local_commits:
+            self._tool.scm().ensure_no_local_commits(self._options.force_clean)
+        self._tool.scm().ensure_clean_working_directory(force_clean=self._options.force_clean)
diff --git a/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectory_unittest.py b/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectory_unittest.py
new file mode 100644
index 0000000..15a8850
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectory_unittest.py
@@ -0,0 +1,52 @@
+# 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.thirdparty.mock import Mock
+from webkitpy.tool.mocktool import MockOptions, MockTool
+from webkitpy.tool.steps.cleanworkingdirectory import CleanWorkingDirectory
+
+
+class CleanWorkingDirectoryTest(unittest.TestCase):
+    def test_run(self):
+        tool = MockTool()
+        tool._scm = Mock()
+        tool._scm.checkout_root = '/mock-checkout'
+        step = CleanWorkingDirectory(tool, MockOptions(clean=True, force_clean=False))
+        step.run({})
+        self.assertEqual(tool._scm.ensure_no_local_commits.call_count, 1)
+        self.assertEqual(tool._scm.ensure_clean_working_directory.call_count, 1)
+
+    def test_no_clean(self):
+        tool = MockTool()
+        tool._scm = Mock()
+        step = CleanWorkingDirectory(tool, MockOptions(clean=False))
+        step.run({})
+        self.assertEqual(tool._scm.ensure_no_local_commits.call_count, 0)
+        self.assertEqual(tool._scm.ensure_clean_working_directory.call_count, 0)
diff --git a/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectorywithlocalcommits.py b/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectorywithlocalcommits.py
new file mode 100644
index 0000000..f06f94e
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectorywithlocalcommits.py
@@ -0,0 +1,34 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.cleanworkingdirectory import CleanWorkingDirectory
+
+class CleanWorkingDirectoryWithLocalCommits(CleanWorkingDirectory):
+    def __init__(self, tool, options):
+        # FIXME: This a bit of a hack.  Consider doing this more cleanly.
+        CleanWorkingDirectory.__init__(self, tool, options, allow_local_commits=True)
diff --git a/Tools/Scripts/webkitpy/tool/steps/closebug.py b/Tools/Scripts/webkitpy/tool/steps/closebug.py
new file mode 100644
index 0000000..b33e373
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/closebug.py
@@ -0,0 +1,53 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+from webkitpy.common.system.deprecated_logging import log
+
+
+class CloseBug(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.close_bug,
+        ]
+
+    def run(self, state):
+        if not self._options.close_bug:
+            return
+        # Check to make sure there are no r? or r+ patches on the bug before closing.
+        # Assume that r- patches are just previous patches someone forgot to obsolete.
+        # FIXME: Should this use self.cached_lookup('bug')?  It's unclear if
+        # state["patch"].bug_id() always equals state['bug_id'].
+        patches = self._tool.bugs.fetch_bug(state["patch"].bug_id()).patches()
+        for patch in patches:
+            if patch.review() == "?" or patch.review() == "+":
+                log("Not closing bug %s as attachment %s has review=%s.  Assuming there are more patches to land from this bug." % (patch.bug_id(), patch.id(), patch.review()))
+                return
+        self._tool.bugs.close_bug_as_fixed(state["patch"].bug_id(), "All reviewed patches have been landed.  Closing bug.")
diff --git a/Tools/Scripts/webkitpy/tool/steps/closebugforlanddiff.py b/Tools/Scripts/webkitpy/tool/steps/closebugforlanddiff.py
new file mode 100644
index 0000000..e5a68db
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/closebugforlanddiff.py
@@ -0,0 +1,58 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.comments import bug_comment_from_commit_text
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+from webkitpy.common.system.deprecated_logging import log
+
+
+class CloseBugForLandDiff(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.close_bug,
+        ]
+
+    def run(self, state):
+        comment_text = bug_comment_from_commit_text(self._tool.scm(), state["commit_text"])
+        bug_id = state.get("bug_id")
+        if not bug_id and state.get("patch"):
+            bug_id = state.get("patch").bug_id()
+
+        if bug_id:
+            log("Updating bug %s" % bug_id)
+            if self._options.close_bug:
+                self._tool.bugs.close_bug_as_fixed(bug_id, comment_text)
+            else:
+                # FIXME: We should a smart way to figure out if the patch is attached
+                # to the bug, and if so obsolete it.
+                self._tool.bugs.post_comment_to_bug(bug_id, comment_text)
+        else:
+            log(comment_text)
+            log("No bug id provided.")
diff --git a/Tools/Scripts/webkitpy/tool/steps/closebugforlanddiff_unittest.py b/Tools/Scripts/webkitpy/tool/steps/closebugforlanddiff_unittest.py
new file mode 100644
index 0000000..0a56564
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/closebugforlanddiff_unittest.py
@@ -0,0 +1,40 @@
+# Copyright (C) 2009 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.system.outputcapture import OutputCapture
+from webkitpy.tool.mocktool import MockOptions, MockTool
+from webkitpy.tool.steps.closebugforlanddiff import CloseBugForLandDiff
+
+class CloseBugForLandDiffTest(unittest.TestCase):
+    def test_empty_state(self):
+        capture = OutputCapture()
+        step = CloseBugForLandDiff(MockTool(), MockOptions())
+        expected_stderr = "Committed r49824: <http://trac.webkit.org/changeset/49824>\nNo bug id provided.\n"
+        capture.assert_outputs(self, step.run, [{"commit_text" : "Mock commit text"}], expected_stderr=expected_stderr)
diff --git a/Tools/Scripts/webkitpy/tool/steps/closepatch.py b/Tools/Scripts/webkitpy/tool/steps/closepatch.py
new file mode 100644
index 0000000..ff94df8
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/closepatch.py
@@ -0,0 +1,36 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.comments import bug_comment_from_commit_text
+from webkitpy.tool.steps.abstractstep import AbstractStep
+
+
+class ClosePatch(AbstractStep):
+    def run(self, state):
+        comment_text = bug_comment_from_commit_text(self._tool.scm(), state["commit_text"])
+        self._tool.bugs.clear_attachment_flags(state["patch"].id(), comment_text)
diff --git a/Tools/Scripts/webkitpy/tool/steps/commit.py b/Tools/Scripts/webkitpy/tool/steps/commit.py
new file mode 100644
index 0000000..0e5ca91
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/commit.py
@@ -0,0 +1,110 @@
+# 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 sys
+
+from webkitpy.common.checkout.scm import AuthenticationError, AmbiguousCommitError
+from webkitpy.common.config import urls
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.system.user import User
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+
+
+class Commit(AbstractStep):
+    # FIXME: This option exists only to make sure we don't break scripts which include --ignore-builders
+    # You can safely delete this option any time after 11/01/11.
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.check_builders,
+            Options.non_interactive,
+        ]
+
+    def _commit_warning(self, error):
+        working_directory_message = "" if error.working_directory_is_clean else " and working copy changes"
+        return ('There are %s local commits%s. Everything will be committed as a single commit. '
+                'To avoid this prompt, set "git config webkit-patch.commit-should-always-squash true".' % (
+                error.num_local_commits, working_directory_message))
+
+    def _check_test_expectations(self, changed_files):
+        test_expectations_files = [filename for filename in changed_files if filename.endswith('TestExpectations')]
+        if not test_expectations_files:
+            return
+
+        args = ["--diff-files"]
+        args.extend(test_expectations_files)
+        try:
+            self._tool.executive.run_and_throw_if_fail(self._tool.port().check_webkit_style_command() + args, cwd=self._tool.scm().checkout_root)
+        except ScriptError, e:
+            if self._options.non_interactive:
+                raise
+            if not self._tool.user.confirm("Are you sure you want to continue?", default="n"):
+                self._exit(1)
+
+    def run(self, state):
+        self._commit_message = self._tool.checkout().commit_message_for_this_commit(self._options.git_commit).message()
+        if len(self._commit_message) < 10:
+            raise Exception("Attempted to commit with a commit message shorter than 10 characters.  Either your patch is missing a ChangeLog or webkit-patch may have a bug.")
+
+        self._check_test_expectations(self._changed_files(state))
+
+        self._state = state
+
+        username = None
+        password = None
+        force_squash = False
+
+        num_tries = 0
+        while num_tries < 3:
+            num_tries += 1
+
+            try:
+                scm = self._tool.scm()
+                commit_text = scm.commit_with_message(self._commit_message, git_commit=self._options.git_commit, username=username, password=password, force_squash=force_squash, changed_files=self._changed_files(state))
+                svn_revision = scm.svn_revision_from_commit_text(commit_text)
+                log("Committed r%s: <%s>" % (svn_revision, urls.view_revision_url(svn_revision)))
+                self._state["commit_text"] = commit_text
+                break;
+            except AmbiguousCommitError, e:
+                if self._options.non_interactive or self._tool.user.confirm(self._commit_warning(e)):
+                    force_squash = True
+                else:
+                    # This will correctly interrupt the rest of the commit process.
+                    raise ScriptError(message="Did not commit")
+            except AuthenticationError, e:
+                if self._options.non_interactive:
+                    raise ScriptError(message="Authentication required")
+                username = self._tool.user.prompt("%s login: " % e.server_host, repeat=5)
+                if not username:
+                    raise ScriptError("You need to specify the username on %s to perform the commit as." % e.server_host)
+                if e.prompt_for_password:
+                    password = self._tool.user.prompt_password("%s password for %s: " % (e.server_host, username), repeat=5)
+                    if not password:
+                        raise ScriptError("You need to specify the password for %s on %s to perform the commit." % (username, e.server_host))
diff --git a/Tools/Scripts/webkitpy/tool/steps/commit_unittest.py b/Tools/Scripts/webkitpy/tool/steps/commit_unittest.py
new file mode 100644
index 0000000..25d9b61
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/commit_unittest.py
@@ -0,0 +1,65 @@
+# Copyright (C) 2012 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.system.outputcapture import OutputCapture
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.system.executive_mock import MockExecutive
+from webkitpy.tool.mocktool import MockOptions, MockTool
+from webkitpy.tool.steps.commit import Commit
+
+
+class CommitTest(unittest.TestCase):
+    def _test_check_test_expectations(self, filename):
+        capture = OutputCapture()
+        options = MockOptions()
+        options.git_commit = ""
+        options.non_interactive = True
+
+        tool = MockTool()
+        tool.user = None  # Will cause any access of tool.user to raise an exception.
+        step = Commit(tool, options)
+        state = {
+            "changed_files": [filename + "XXX"],
+        }
+
+        tool.executive = MockExecutive(should_log=True, should_throw_when_run=False)
+        capture.assert_outputs(self, step.run, [state], expected_stderr="Committed r49824: <http://trac.webkit.org/changeset/49824>\n")
+
+        state = {
+            "changed_files": ["platform/chromium/" + filename],
+        }
+        capture.assert_outputs(self, step.run, [state], expected_stderr="MOCK run_and_throw_if_fail: ['mock-check-webkit-style', '--diff-files', 'platform/chromium/"
+            + filename + "'], cwd=/mock-checkout\nCommitted r49824: <http://trac.webkit.org/changeset/49824>\n")
+
+        tool.executive = MockExecutive(should_log=True, should_throw_when_run=set(["platform/chromium/" + filename]))
+        self.assertRaises(ScriptError, capture.assert_outputs, self, step.run, [state])
+
+    def test_check_test_expectations(self):
+        self._test_check_test_expectations('TestExpectations')
diff --git a/Tools/Scripts/webkitpy/tool/steps/confirmdiff.py b/Tools/Scripts/webkitpy/tool/steps/confirmdiff.py
new file mode 100644
index 0000000..86c8a2c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/confirmdiff.py
@@ -0,0 +1,77 @@
+# 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 urllib
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+from webkitpy.common.prettypatch import PrettyPatch
+from webkitpy.common.system import logutils
+from webkitpy.common.system.executive import ScriptError
+
+
+_log = logutils.get_logger(__file__)
+
+
+class ConfirmDiff(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.confirm,
+        ]
+
+    def _show_pretty_diff(self, diff):
+        if not self._tool.user.can_open_url():
+            return None
+
+        try:
+            pretty_patch = PrettyPatch(self._tool.executive,
+                                       self._tool.scm().checkout_root)
+            pretty_diff_file = pretty_patch.pretty_diff_file(diff)
+            url = "file://%s" % urllib.quote(pretty_diff_file.name)
+            self._tool.user.open_url(url)
+            # We return the pretty_diff_file here because we need to keep the
+            # file alive until the user has had a chance to confirm the diff.
+            return pretty_diff_file
+        except ScriptError, e:
+            _log.warning("PrettyPatch failed.  :(")
+        except OSError, e:
+            _log.warning("PrettyPatch unavailable.")
+
+    def run(self, state):
+        if not self._options.confirm:
+            return
+        diff = self.cached_lookup(state, "diff")
+        pretty_diff_file = self._show_pretty_diff(diff)
+        if not pretty_diff_file:
+            self._tool.user.page(diff)
+        diff_correct = self._tool.user.confirm("Was that diff correct?")
+        if pretty_diff_file:
+            pretty_diff_file.close()
+        if not diff_correct:
+            self._exit(1)
diff --git a/Tools/Scripts/webkitpy/tool/steps/createbug.py b/Tools/Scripts/webkitpy/tool/steps/createbug.py
new file mode 100644
index 0000000..7e4a835
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/createbug.py
@@ -0,0 +1,56 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+
+
+class CreateBug(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.cc,
+            Options.component,
+            Options.blocks,
+        ]
+
+    def run(self, state):
+        # No need to create a bug if we already have one.
+        if state.get("bug_id"):
+            return
+        cc = self._options.cc
+        if not cc:
+            cc = state.get("bug_cc")
+        blocks = self._options.blocks
+        if not blocks:
+            blocks = state.get("bug_blocked")
+        state["bug_id"] = self._tool.bugs.create_bug(state["bug_title"], state["bug_description"], blocked=blocks, component=self._options.component, cc=cc)
+        if blocks:
+            status = self._tool.bugs.fetch_bug(blocks).status()
+            if status == 'RESOLVED':
+                self._tool.bugs.reopen_bug(blocks, "Re-opened since this is blocked by bug %s" % state["bug_id"])
diff --git a/Tools/Scripts/webkitpy/tool/steps/editchangelog.py b/Tools/Scripts/webkitpy/tool/steps/editchangelog.py
new file mode 100644
index 0000000..35cd504
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/editchangelog.py
@@ -0,0 +1,36 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+
+
+class EditChangeLog(AbstractStep):
+    def run(self, state):
+        absolute_paths = map(self._tool.scm().absolute_path, self.cached_lookup(state, "changelogs"))
+        self._tool.user.edit_changelog(absolute_paths)
+        self.did_modify_checkout(state)
diff --git a/Tools/Scripts/webkitpy/tool/steps/ensurebugisopenandassigned.py b/Tools/Scripts/webkitpy/tool/steps/ensurebugisopenandassigned.py
new file mode 100644
index 0000000..54f90b6
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/ensurebugisopenandassigned.py
@@ -0,0 +1,41 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+
+
+class EnsureBugIsOpenAndAssigned(AbstractStep):
+    def run(self, state):
+        bug = self.cached_lookup(state, "bug")
+        if bug.is_unassigned():
+            self._tool.bugs.reassign_bug(bug.id())
+
+        if bug.is_closed():
+            # FIXME: We should probably pass this message in somehow?
+            # Right now this step is only used before PostDiff steps, so this is OK.
+            self._tool.bugs.reopen_bug(bug.id(), "Reopening to attach new patch.")
diff --git a/Tools/Scripts/webkitpy/tool/steps/ensurelocalcommitifneeded.py b/Tools/Scripts/webkitpy/tool/steps/ensurelocalcommitifneeded.py
new file mode 100644
index 0000000..2167351
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/ensurelocalcommitifneeded.py
@@ -0,0 +1,43 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+from webkitpy.common.system.deprecated_logging import error
+
+
+class EnsureLocalCommitIfNeeded(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.local_commit,
+        ]
+
+    def run(self, state):
+        if self._options.local_commit and not self._tool.scm().supports_local_commits():
+            error("--local-commit passed, but %s does not support local commits" % self._tool.scm().display_name())
diff --git a/Tools/Scripts/webkitpy/tool/steps/metastep.py b/Tools/Scripts/webkitpy/tool/steps/metastep.py
new file mode 100644
index 0000000..7cbd1c5
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/metastep.py
@@ -0,0 +1,54 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+
+
+# FIXME: Unify with StepSequence?  I'm not sure yet which is the better design.
+class MetaStep(AbstractStep):
+    substeps = [] # Override in subclasses
+    def __init__(self, tool, options):
+        AbstractStep.__init__(self, tool, options)
+        self._step_instances = []
+        for step_class in self.substeps:
+            self._step_instances.append(step_class(tool, options))
+
+    @staticmethod
+    def _collect_options_from_steps(steps):
+        collected_options = []
+        for step in steps:
+            collected_options = collected_options + step.options()
+        return collected_options
+
+    @classmethod
+    def options(cls):
+        return cls._collect_options_from_steps(cls.substeps)
+
+    def run(self, state):
+        for step in self._step_instances:
+             step.run(state)
diff --git a/Tools/Scripts/webkitpy/tool/steps/obsoletepatches.py b/Tools/Scripts/webkitpy/tool/steps/obsoletepatches.py
new file mode 100644
index 0000000..de508c6
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/obsoletepatches.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.grammar import pluralize
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+from webkitpy.common.system.deprecated_logging import log
+
+
+class ObsoletePatches(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.obsolete_patches,
+        ]
+
+    def run(self, state):
+        if not self._options.obsolete_patches:
+            return
+        bug_id = state["bug_id"]
+        patches = self._tool.bugs.fetch_bug(bug_id).patches()
+        if not patches:
+            return
+        log("Obsoleting %s on bug %s" % (pluralize("old patch", len(patches)), bug_id))
+        for patch in patches:
+            self._tool.bugs.obsolete_attachment(patch.id())
diff --git a/Tools/Scripts/webkitpy/tool/steps/options.py b/Tools/Scripts/webkitpy/tool/steps/options.py
new file mode 100644
index 0000000..c29e59d
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/options.py
@@ -0,0 +1,60 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from optparse import make_option
+
+class Options(object):
+    blocks = make_option("--blocks", action="store", type="string", dest="blocks", default=None, help="Bug number which the created bug blocks.")
+    build = make_option("--build", action="store_true", dest="build", default=False, help="Build and run run-webkit-tests before committing.")
+    build_style = make_option("--build-style", action="store", dest="build_style", default=None, help="Whether to build debug, release, or both.")
+    cc = make_option("--cc", action="store", type="string", dest="cc", help="Comma-separated list of email addresses to carbon-copy.")
+    check_builders = make_option("--ignore-builders", action="store_false", dest="check_builders", default=True, help="DEPRECATED: Will be removed any time after 11/01/11.")
+    check_style = make_option("--ignore-style", action="store_false", dest="check_style", default=True, help="Don't check to see if the patch has proper style before uploading.")
+    check_style_filter = make_option("--check-style-filter", action="store", type="string", dest="check_style_filter", default=None, help="Filter style-checker rules (see check-webkit-style --help).")
+    clean = make_option("--no-clean", action="store_false", dest="clean", default=True, help="Don't check if the working directory is clean before applying patches")
+    close_bug = make_option("--no-close", action="store_false", dest="close_bug", default=True, help="Leave bug open after landing.")
+    comment = make_option("--comment", action="store", type="string", dest="comment", help="Comment to post to bug.")
+    component = make_option("--component", action="store", type="string", dest="component", help="Component for the new bug.")
+    confirm = make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Skip confirmation steps.")
+    description = make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment")
+    email = make_option("--email", action="store", type="string", dest="email", help="Email address to use in ChangeLogs.")
+    force_clean = make_option("--force-clean", action="store_true", dest="force_clean", default=False, help="Clean working directory before applying patches (removes local changes and commits)")
+    git_commit = make_option("-g", "--git-commit", action="store", dest="git_commit", help="Operate on a local commit. If a range, the commits are squashed into one. <ref>.... includes the working copy changes. UPSTREAM can be used for the upstream/tracking branch.")
+    local_commit = make_option("--local-commit", action="store_true", dest="local_commit", default=False, help="Make a local commit for each applied patch")
+    non_interactive = make_option("--non-interactive", action="store_true", dest="non_interactive", default=False, help="Never prompt the user, fail as fast as possible.")
+    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.")
+    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.")
+    reviewer = make_option("-r", "--reviewer", action="store", type="string", dest="reviewer", help="Update ChangeLogs to say Reviewed by REVIEWER.")
+    suggest_reviewers = make_option("--suggest-reviewers", action="store_true", default=False, help="Offer to CC appropriate reviewers.")
+    test = make_option("--test", action="store_true", dest="test", default=False, help="Run run-webkit-tests before committing.")
+    update = make_option("--no-update", action="store_false", dest="update", default=True, help="Don't update the working directory.")
+    changelog_count = make_option("--changelog-count", action="store", type="int", dest="changelog_count", help="Number of changelogs to parse.")
diff --git a/Tools/Scripts/webkitpy/tool/steps/postdiff.py b/Tools/Scripts/webkitpy/tool/steps/postdiff.py
new file mode 100644
index 0000000..6913cab
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/postdiff.py
@@ -0,0 +1,52 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+
+
+class PostDiff(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.description,
+            Options.comment,
+            Options.review,
+            Options.request_commit,
+            Options.open_bug,
+        ]
+
+    def run(self, state):
+        diff = self.cached_lookup(state, "diff")
+        description = self._options.description or "Patch"
+        comment_text = self._options.comment
+        bug_id = state["bug_id"]
+
+        self._tool.bugs.add_patch_to_bug(bug_id, diff, description, comment_text=comment_text, mark_for_review=self._options.review, mark_for_commit_queue=self._options.request_commit)
+        if self._options.open_bug:
+            self._tool.user.open_url(self._tool.bugs.bug_url_for_bug_id(bug_id))
diff --git a/Tools/Scripts/webkitpy/tool/steps/postdiffforcommit.py b/Tools/Scripts/webkitpy/tool/steps/postdiffforcommit.py
new file mode 100644
index 0000000..13bc00c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/postdiffforcommit.py
@@ -0,0 +1,39 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+
+
+class PostDiffForCommit(AbstractStep):
+    def run(self, state):
+        self._tool.bugs.add_patch_to_bug(
+            state["bug_id"],
+            self.cached_lookup(state, "diff"),
+            "Patch for landing",
+            mark_for_review=False,
+            mark_for_landing=True)
diff --git a/Tools/Scripts/webkitpy/tool/steps/postdiffforrevert.py b/Tools/Scripts/webkitpy/tool/steps/postdiffforrevert.py
new file mode 100644
index 0000000..2900eb3
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/postdiffforrevert.py
@@ -0,0 +1,49 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.net.bugzilla import Attachment
+from webkitpy.tool.steps.abstractstep import AbstractStep
+
+
+class PostDiffForRevert(AbstractStep):
+    def run(self, state):
+        comment_text = "Any committer can land this patch automatically by \
+marking it commit-queue+.  The commit-queue will build and test \
+the patch before landing to ensure that the rollout will be \
+successful.  This process takes approximately 15 minutes.\n\n\
+If you would like to land the rollout faster, you can use the \
+following command:\n\n\
+  webkit-patch land-attachment ATTACHMENT_ID\n\n\
+where ATTACHMENT_ID is the ID of this attachment."
+        self._tool.bugs.add_patch_to_bug(
+            state["bug_id"],
+            self.cached_lookup(state, "diff"),
+            "%s%s" % (Attachment.rollout_preamble, state["revision"]),
+            comment_text=comment_text,
+            mark_for_review=False,
+            mark_for_commit_queue=True)
diff --git a/Tools/Scripts/webkitpy/tool/steps/preparechangelog.py b/Tools/Scripts/webkitpy/tool/steps/preparechangelog.py
new file mode 100644
index 0000000..19caace
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/preparechangelog.py
@@ -0,0 +1,77 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.checkout.changelog import ChangeLog
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+from webkitpy.common.system.deprecated_logging import error
+
+
+class PrepareChangeLog(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.quiet,
+            Options.email,
+            Options.git_commit,
+        ]
+
+    def _ensure_bug_url(self, state):
+        if not state.get("bug_id"):
+            return
+        bug_id = state.get("bug_id")
+        changelogs = self.cached_lookup(state, "changelogs")
+        for changelog_path in changelogs:
+            changelog = ChangeLog(changelog_path)
+            if not changelog.latest_entry().bug_id():
+                changelog.set_short_description_and_bug_url(
+                    self.cached_lookup(state, "bug_title"),
+                    self._tool.bugs.bug_url_for_bug_id(bug_id))
+
+    def run(self, state):
+        if self.cached_lookup(state, "changelogs"):
+            self._ensure_bug_url(state)
+            return
+        args = self._tool.port().prepare_changelog_command()
+        if state.get("bug_id"):
+            args.append("--bug=%s" % state["bug_id"])
+            args.append("--description=%s" % self.cached_lookup(state, 'bug_title'))
+        if self._options.email:
+            args.append("--email=%s" % self._options.email)
+
+        if self._tool.scm().supports_local_commits():
+            args.append("--merge-base=%s" % self._tool.scm().merge_base(self._options.git_commit))
+
+        args.extend(self._changed_files(state))
+
+        try:
+            self._tool.executive.run_and_throw_if_fail(args, self._options.quiet, cwd=self._tool.scm().checkout_root)
+        except ScriptError, e:
+            error("Unable to prepare ChangeLogs.")
+        self.did_modify_checkout(state)
diff --git a/Tools/Scripts/webkitpy/tool/steps/preparechangelog_unittest.py b/Tools/Scripts/webkitpy/tool/steps/preparechangelog_unittest.py
new file mode 100644
index 0000000..847dc2f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/preparechangelog_unittest.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import unittest
+
+# Do not import changelog_unittest.ChangeLogTest directly as that will cause it to be run again.
+from webkitpy.common.checkout import changelog_unittest
+
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.tool.mocktool import MockOptions, MockTool
+from webkitpy.tool.steps.preparechangelog import PrepareChangeLog
+
+
+class PrepareChangeLogTest(changelog_unittest.ChangeLogTest):
+    def test_ensure_bug_url(self):
+        # FIXME: This should use a MockFileSystem instead of a real FileSystem.
+        capture = OutputCapture()
+        step = PrepareChangeLog(MockTool(), MockOptions())
+        changelog_contents = u"%s\n%s" % (self._new_entry_boilerplate, self._example_changelog)
+        changelog_path = self._write_tmp_file_with_contents(changelog_contents.encode("utf-8"))
+        state = {
+            "bug_title": "Example title",
+            "bug_id": 1234,
+            "changelogs": [changelog_path],
+        }
+        capture.assert_outputs(self, step.run, [state])
+        actual_contents = self._read_file_contents(changelog_path, "utf-8")
+        expected_message = "Example title\n        http://example.com/1234"
+        expected_contents = changelog_contents.replace("Need a short description (OOPS!).\n        Need the bug URL (OOPS!).", expected_message)
+        os.remove(changelog_path)
+        self.assertEquals(actual_contents.splitlines(), expected_contents.splitlines())
diff --git a/Tools/Scripts/webkitpy/tool/steps/preparechangelogfordepsroll.py b/Tools/Scripts/webkitpy/tool/steps/preparechangelogfordepsroll.py
new file mode 100644
index 0000000..4bbd383
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/preparechangelogfordepsroll.py
@@ -0,0 +1,38 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.checkout.changelog import ChangeLog
+from webkitpy.tool.steps.abstractstep import AbstractStep
+
+
+class PrepareChangeLogForDEPSRoll(AbstractStep):
+    def run(self, state):
+        self._tool.executive.run_and_throw_if_fail(self._tool.port().prepare_changelog_command())
+        changelog_paths = self._tool.checkout().modified_changelogs(git_commit=None)
+        for changelog_path in changelog_paths:
+            ChangeLog(changelog_path).update_with_unreviewed_message("Unreviewed.  Rolled DEPS.\n\n")
diff --git a/Tools/Scripts/webkitpy/tool/steps/preparechangelogforrevert.py b/Tools/Scripts/webkitpy/tool/steps/preparechangelogforrevert.py
new file mode 100644
index 0000000..95a99c3
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/preparechangelogforrevert.py
@@ -0,0 +1,58 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.checkout.changelog import ChangeLog
+from webkitpy.common.config import urls
+from webkitpy.tool.grammar import join_with_separators
+from webkitpy.tool.steps.abstractstep import AbstractStep
+
+
+class PrepareChangeLogForRevert(AbstractStep):
+    @classmethod
+    def _message_for_revert(cls, revision_list, reason, bug_url=None):
+        message = "Unreviewed, rolling out %s.\n" % join_with_separators(['r' + str(revision) for revision in revision_list])
+        for revision in revision_list:
+            message += "%s\n" % urls.view_revision_url(revision)
+        if bug_url:
+            message += "%s\n" % bug_url
+        # Add an extra new line after the rollout links, before any reason.
+        message += "\n"
+        if reason:
+            message += "%s\n\n" % reason
+        return message
+
+    def run(self, state):
+        # This could move to prepare-ChangeLog by adding a --revert= option.
+        self._tool.executive.run_and_throw_if_fail(self._tool.port().prepare_changelog_command(), cwd=self._tool.scm().checkout_root)
+        changelog_paths = self._tool.checkout().modified_changelogs(git_commit=None)
+        bug_url = self._tool.bugs.bug_url_for_bug_id(state["bug_id"]) if state["bug_id"] else None
+        message = self._message_for_revert(state["revision_list"], state["reason"], bug_url)
+        for changelog_path in changelog_paths:
+            # FIXME: Seems we should prepare the message outside of changelogs.py and then just pass in
+            # text that we want to use to replace the reviewed by line.
+            ChangeLog(changelog_path).update_with_unreviewed_message(message)
diff --git a/Tools/Scripts/webkitpy/tool/steps/preparechangelogforrevert_unittest.py b/Tools/Scripts/webkitpy/tool/steps/preparechangelogforrevert_unittest.py
new file mode 100644
index 0000000..076e602
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/preparechangelogforrevert_unittest.py
@@ -0,0 +1,130 @@
+# Copyright (C) 2011 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 codecs
+import os
+import tempfile
+import unittest
+
+# Do not import changelog_unittest.ChangeLogTest directly as that will cause it to be run again.
+from webkitpy.common.checkout import changelog_unittest
+
+from webkitpy.common.checkout.changelog import ChangeLog
+from webkitpy.tool.steps.preparechangelogforrevert import *
+
+
+class UpdateChangeLogsForRevertTest(unittest.TestCase):
+    @staticmethod
+    def _write_tmp_file_with_contents(byte_array):
+        assert(isinstance(byte_array, str))
+        (file_descriptor, file_path) = tempfile.mkstemp()  # NamedTemporaryFile always deletes the file on close in python < 2.6
+        with os.fdopen(file_descriptor, "w") as file:
+            file.write(byte_array)
+        return file_path
+
+    _revert_entry_with_bug_url = '''2009-08-19  Eric Seidel  <eric@webkit.org>
+
+        Unreviewed, rolling out r12345.
+        http://trac.webkit.org/changeset/12345
+        http://example.com/123
+
+        Reason
+
+        * Scripts/bugzilla-tool:
+'''
+
+    _revert_entry_without_bug_url = '''2009-08-19  Eric Seidel  <eric@webkit.org>
+
+        Unreviewed, rolling out r12345.
+        http://trac.webkit.org/changeset/12345
+
+        Reason
+
+        * Scripts/bugzilla-tool:
+'''
+
+    _multiple_revert_entry_with_bug_url = '''2009-08-19  Eric Seidel  <eric@webkit.org>
+
+        Unreviewed, rolling out r12345, r12346, and r12347.
+        http://trac.webkit.org/changeset/12345
+        http://trac.webkit.org/changeset/12346
+        http://trac.webkit.org/changeset/12347
+        http://example.com/123
+
+        Reason
+
+        * Scripts/bugzilla-tool:
+'''
+
+    _multiple_revert_entry_without_bug_url = '''2009-08-19  Eric Seidel  <eric@webkit.org>
+
+        Unreviewed, rolling out r12345, r12346, and r12347.
+        http://trac.webkit.org/changeset/12345
+        http://trac.webkit.org/changeset/12346
+        http://trac.webkit.org/changeset/12347
+
+        Reason
+
+        * Scripts/bugzilla-tool:
+'''
+
+    _revert_with_log_reason = """2009-08-19  Eric Seidel  <eric@webkit.org>
+
+        Unreviewed, rolling out r12345.
+        http://trac.webkit.org/changeset/12345
+        http://example.com/123
+
+        This is a very long reason which should be long enough so that
+        _message_for_revert will need to wrap it.  We'll also include
+        a
+        https://veryveryveryveryverylongbugurl.com/reallylongbugthingy.cgi?bug_id=12354
+        link so that we can make sure we wrap that right too.
+
+        * Scripts/bugzilla-tool:
+"""
+
+    def _assert_message_for_revert_output(self, args, expected_entry):
+        changelog_contents = u"%s\n%s" % (changelog_unittest.ChangeLogTest._new_entry_boilerplate, changelog_unittest.ChangeLogTest._example_changelog)
+        changelog_path = self._write_tmp_file_with_contents(changelog_contents.encode("utf-8"))
+        changelog = ChangeLog(changelog_path)
+        changelog.update_with_unreviewed_message(PrepareChangeLogForRevert._message_for_revert(*args))
+        actual_entry = changelog.latest_entry()
+        os.remove(changelog_path)
+        self.assertEquals(actual_entry.contents(), expected_entry)
+        self.assertEquals(actual_entry.reviewer_text(), None)
+        # These checks could be removed to allow this to work on other entries:
+        self.assertEquals(actual_entry.author_name(), "Eric Seidel")
+        self.assertEquals(actual_entry.author_email(), "eric@webkit.org")
+
+    def test_message_for_revert(self):
+        self._assert_message_for_revert_output([[12345], "Reason"], self._revert_entry_without_bug_url)
+        self._assert_message_for_revert_output([[12345], "Reason", "http://example.com/123"], self._revert_entry_with_bug_url)
+        self._assert_message_for_revert_output([[12345, 12346, 12347], "Reason"], self._multiple_revert_entry_without_bug_url)
+        self._assert_message_for_revert_output([[12345, 12346, 12347], "Reason", "http://example.com/123"], self._multiple_revert_entry_with_bug_url)
+        long_reason = "This is a very long reason which should be long enough so that _message_for_revert will need to wrap it.  We'll also include a https://veryveryveryveryverylongbugurl.com/reallylongbugthingy.cgi?bug_id=12354 link so that we can make sure we wrap that right too."
+        self._assert_message_for_revert_output([[12345], long_reason, "http://example.com/123"], self._revert_with_log_reason)
diff --git a/Tools/Scripts/webkitpy/tool/steps/promptforbugortitle.py b/Tools/Scripts/webkitpy/tool/steps/promptforbugortitle.py
new file mode 100644
index 0000000..31c913c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/promptforbugortitle.py
@@ -0,0 +1,45 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+
+
+class PromptForBugOrTitle(AbstractStep):
+    def run(self, state):
+        # No need to prompt if we alrady have the bug_id.
+        if state.get("bug_id"):
+            return
+        user_response = self._tool.user.prompt("Please enter a bug number or a title for a new bug:\n")
+        # If the user responds with a number, we assume it's bug number.
+        # Otherwise we assume it's a bug subject.
+        try:
+            state["bug_id"] = int(user_response)
+        except ValueError, TypeError:
+            state["bug_title"] = user_response
+            # FIXME: This is kind of a lame description.
+            state["bug_description"] = user_response
diff --git a/Tools/Scripts/webkitpy/tool/steps/reopenbugafterrollout.py b/Tools/Scripts/webkitpy/tool/steps/reopenbugafterrollout.py
new file mode 100644
index 0000000..f369ca9
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/reopenbugafterrollout.py
@@ -0,0 +1,44 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.comments import bug_comment_from_commit_text
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.common.system.deprecated_logging import log
+
+
+class ReopenBugAfterRollout(AbstractStep):
+    def run(self, state):
+        commit_comment = bug_comment_from_commit_text(self._tool.scm(), state["commit_text"])
+        comment_text = "Reverted r%s for reason:\n\n%s\n\n%s" % (state["revision"], state["reason"], commit_comment)
+
+        bug_id = state["bug_id"]
+        if not bug_id:
+            log(comment_text)
+            log("No bugs were updated.")
+            return
+        self._tool.bugs.reopen_bug(bug_id, comment_text)
diff --git a/Tools/Scripts/webkitpy/tool/steps/revertrevision.py b/Tools/Scripts/webkitpy/tool/steps/revertrevision.py
new file mode 100644
index 0000000..8016be5
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/revertrevision.py
@@ -0,0 +1,35 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+
+
+class RevertRevision(AbstractStep):
+    def run(self, state):
+        self._tool.checkout().apply_reverse_diffs(state["revision_list"])
+        self.did_modify_checkout(state)
diff --git a/Tools/Scripts/webkitpy/tool/steps/runtests.py b/Tools/Scripts/webkitpy/tool/steps/runtests.py
new file mode 100644
index 0000000..aa87291
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/runtests.py
@@ -0,0 +1,93 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.common.system.executive import ScriptError
+
+class RunTests(AbstractStep):
+    # FIXME: This knowledge really belongs in the commit-queue.
+    NON_INTERACTIVE_FAILURE_LIMIT_COUNT = 30
+
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.test,
+            Options.non_interactive,
+            Options.quiet,
+        ]
+
+    def run(self, state):
+        if not self._options.test:
+            return
+
+        if not self._options.non_interactive:
+            # FIXME: We should teach the commit-queue and the EWS how to run these tests.
+
+            python_unittests_command = self._tool.port().run_python_unittests_command()
+            if python_unittests_command:
+                log("Running Python unit tests")
+                self._tool.executive.run_and_throw_if_fail(python_unittests_command, cwd=self._tool.scm().checkout_root)
+
+            perl_unittests_command = self._tool.port().run_perl_unittests_command()
+            if perl_unittests_command:
+                log("Running Perl unit tests")
+                self._tool.executive.run_and_throw_if_fail(perl_unittests_command, cwd=self._tool.scm().checkout_root)
+
+            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, cwd=self._tool.scm().checkout_root)
+
+        webkit_unit_tests_command = self._tool.port().run_webkit_unit_tests_command()
+        if webkit_unit_tests_command:
+            log("Running WebKit unit tests")
+            args = webkit_unit_tests_command
+            if self._options.non_interactive:
+                args.append("--gtest_output=xml:%s/webkit_unit_tests_output.xml" % self._tool.port().results_directory)
+            try:
+                self._tool.executive.run_and_throw_if_fail(args, cwd=self._tool.scm().checkout_root)
+            except ScriptError, e:
+                log("Error running webkit_unit_tests: %s" % e.message_with_output())
+
+        log("Running run-webkit-tests")
+        args = self._tool.port().run_webkit_tests_command()
+        if self._options.non_interactive:
+            args.extend([
+                "--no-new-test-results",
+                "--no-launch-safari",
+                "--skip-failing-tests",
+                "--exit-after-n-failures=%s" % self.NON_INTERACTIVE_FAILURE_LIMIT_COUNT,
+                "--results-directory=%s" % self._tool.port().results_directory,
+                "--quiet",
+            ])
+
+        if self._options.quiet:
+            args.append("--quiet")
+        self._tool.executive.run_and_throw_if_fail(args, cwd=self._tool.scm().checkout_root)
diff --git a/Tools/Scripts/webkitpy/tool/steps/runtests_unittest.py b/Tools/Scripts/webkitpy/tool/steps/runtests_unittest.py
new file mode 100644
index 0000000..bf888e5
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/runtests_unittest.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2011 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.system.outputcapture import OutputCapture
+from webkitpy.tool.mocktool import MockOptions, MockTool
+from webkitpy.tool.steps.runtests import RunTests
+
+class RunTestsTest(unittest.TestCase):
+    def test_webkit_run_unit_tests(self):
+        tool = MockTool(log_executive=True)
+        tool._deprecated_port.run_python_unittests_command = lambda: None
+        tool._deprecated_port.run_perl_unittests_command = lambda: None
+        step = RunTests(tool, MockOptions(test=True, non_interactive=True, quiet=False))
+        expected_stderr = """Running WebKit unit tests
+MOCK run_and_throw_if_fail: ['mock-run-webkit-unit-tests', '--gtest_output=xml:/mock-results/webkit_unit_tests_output.xml'], cwd=/mock-checkout
+Running run-webkit-tests
+MOCK run_and_throw_if_fail: ['mock-run-webkit-tests', '--no-new-test-results', '--no-launch-safari', '--skip-failing-tests', '--exit-after-n-failures=30', '--results-directory=/mock-results', '--quiet'], cwd=/mock-checkout
+"""
+        OutputCapture().assert_outputs(self, step.run, [{}], expected_stderr=expected_stderr)
diff --git a/Tools/Scripts/webkitpy/tool/steps/steps_unittest.py b/Tools/Scripts/webkitpy/tool/steps/steps_unittest.py
new file mode 100644
index 0000000..99f1749
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/steps_unittest.py
@@ -0,0 +1,116 @@
+# 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.system.outputcapture import OutputCapture
+from webkitpy.common.config.ports import DeprecatedPort
+from webkitpy.tool.mocktool import MockOptions, MockTool
+
+from webkitpy.tool import steps
+
+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 = self._step_options()
+        if not state:
+            state = {}
+        step(tool, options).run(state)
+
+    def test_update_step(self):
+        tool = MockTool()
+        options = self._step_options()
+        options.update = True
+        expected_stderr = "Updating working directory\n"
+        OutputCapture().assert_outputs(self, self._run_step, [steps.Update, tool, options], expected_stderr=expected_stderr)
+
+    def test_prompt_for_bug_or_title_step(self):
+        tool = MockTool()
+        tool.user.prompt = lambda message: 50000
+        self._run_step(steps.PromptForBugOrTitle, tool=tool)
+
+    def _post_diff_options(self):
+        options = self._step_options()
+        options.git_commit = None
+        options.description = None
+        options.comment = None
+        options.review = True
+        options.request_commit = False
+        options.open_bug = True
+        return options
+
+    def _assert_step_output_with_bug(self, step, bug_id, expected_stderr, options=None):
+        state = {'bug_id': bug_id}
+        OutputCapture().assert_outputs(self, self._run_step, [step, MockTool(), options, state], expected_stderr=expected_stderr)
+
+    def _assert_post_diff_output_for_bug(self, step, bug_id, expected_stderr):
+        self._assert_step_output_with_bug(step, bug_id, expected_stderr, self._post_diff_options())
+
+    def test_post_diff(self):
+        expected_stderr = "MOCK add_patch_to_bug: bug_id=78, description=Patch, mark_for_review=True, mark_for_commit_queue=False, mark_for_landing=False\nMOCK: user.open_url: http://example.com/78\n"
+        self._assert_post_diff_output_for_bug(steps.PostDiff, 78, expected_stderr)
+
+    def test_post_diff_for_commit(self):
+        expected_stderr = "MOCK add_patch_to_bug: bug_id=78, description=Patch for landing, mark_for_review=False, mark_for_commit_queue=False, mark_for_landing=True\n"
+        self._assert_post_diff_output_for_bug(steps.PostDiffForCommit, 78, expected_stderr)
+
+    def test_ensure_bug_is_open_and_assigned(self):
+        expected_stderr = "MOCK reopen_bug 50004 with comment 'Reopening to attach new patch.'\n"
+        self._assert_step_output_with_bug(steps.EnsureBugIsOpenAndAssigned, 50004, expected_stderr)
+        expected_stderr = "MOCK reassign_bug: bug_id=50002, assignee=None\n"
+        self._assert_step_output_with_bug(steps.EnsureBugIsOpenAndAssigned, 50002, expected_stderr)
+
+    def test_runtests_args(self):
+        mock_options = self._step_options()
+        mock_options.non_interactive = False
+        step = steps.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 = DeprecatedPort()
+        tool = MockTool(log_executive=True)
+        tool.port = lambda: mock_port
+        step = steps.RunTests(tool, mock_options)
+        expected_stderr = """Running Python unit tests
+MOCK run_and_throw_if_fail: ['Tools/Scripts/test-webkitpy'], cwd=/mock-checkout
+Running Perl unit tests
+MOCK run_and_throw_if_fail: ['Tools/Scripts/test-webkitperl'], cwd=/mock-checkout
+Running JavaScriptCore tests
+MOCK run_and_throw_if_fail: ['Tools/Scripts/run-javascriptcore-tests'], cwd=/mock-checkout
+Running run-webkit-tests
+MOCK run_and_throw_if_fail: ['Tools/Scripts/run-webkit-tests', '--quiet'], cwd=/mock-checkout
+"""
+        OutputCapture().assert_outputs(self, step.run, [{}], expected_stderr=expected_stderr)
diff --git a/Tools/Scripts/webkitpy/tool/steps/suggestreviewers.py b/Tools/Scripts/webkitpy/tool/steps/suggestreviewers.py
new file mode 100644
index 0000000..76bef35
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/suggestreviewers.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+
+
+class SuggestReviewers(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.git_commit,
+            Options.suggest_reviewers,
+        ]
+
+    def run(self, state):
+        if not self._options.suggest_reviewers:
+            return
+
+        reviewers = self._tool.checkout().suggested_reviewers(self._options.git_commit, self._changed_files(state))
+        print "The following reviewers have recently modified files in your patch:"
+        print "\n".join([reviewer.full_name for reviewer in reviewers])
+        if not self._tool.user.confirm("Would you like to CC them?"):
+            return
+        reviewer_emails = [reviewer.bugzilla_email() for reviewer in reviewers]
+        self._tool.bugs.add_cc_to_bug(state['bug_id'], reviewer_emails)
diff --git a/Tools/Scripts/webkitpy/tool/steps/suggestreviewers_unittest.py b/Tools/Scripts/webkitpy/tool/steps/suggestreviewers_unittest.py
new file mode 100644
index 0000000..e995663
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/suggestreviewers_unittest.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2009 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.system.outputcapture import OutputCapture
+from webkitpy.tool.mocktool import MockOptions, MockTool
+from webkitpy.tool.steps.suggestreviewers import SuggestReviewers
+
+
+class SuggestReviewersTest(unittest.TestCase):
+    def test_disabled(self):
+        step = SuggestReviewers(MockTool(), MockOptions(suggest_reviewers=False))
+        OutputCapture().assert_outputs(self, step.run, [{}])
+
+    def test_basic(self):
+        capture = OutputCapture()
+        step = SuggestReviewers(MockTool(), MockOptions(suggest_reviewers=True, git_commit=None))
+        expected_stdout = "The following reviewers have recently modified files in your patch:\nFoo Bar\n"
+        expected_stderr = "Would you like to CC them?\n"
+        capture.assert_outputs(self, step.run, [{"bug_id": "123"}], expected_stdout=expected_stdout, expected_stderr=expected_stderr)
diff --git a/Tools/Scripts/webkitpy/tool/steps/update.py b/Tools/Scripts/webkitpy/tool/steps/update.py
new file mode 100644
index 0000000..cae2bbd
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/update.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+from webkitpy.common.system.deprecated_logging import log
+
+
+class Update(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.non_interactive,
+            Options.update,
+            Options.quiet,
+        ]
+
+    def run(self, state):
+        if not self._options.update:
+            return
+        log("Updating working directory")
+        self._tool.executive.run_and_throw_if_fail(self._update_command(), quiet=self._options.quiet, cwd=self._tool.scm().checkout_root)
+
+    def _update_command(self):
+        update_command = self._tool.port().update_webkit_command(self._options.non_interactive)
+        return update_command
diff --git a/Tools/Scripts/webkitpy/tool/steps/update_unittest.py b/Tools/Scripts/webkitpy/tool/steps/update_unittest.py
new file mode 100644
index 0000000..c1a934d
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/update_unittest.py
@@ -0,0 +1,66 @@
+# Copyright (C) 2011 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.config.ports import ChromiumPort, ChromiumAndroidPort, ChromiumXVFBPort
+from webkitpy.tool.mocktool import MockOptions, MockTool
+from webkitpy.tool.steps.update import Update
+
+
+class UpdateTest(unittest.TestCase):
+
+    def test_update_command_non_interactive(self):
+        tool = MockTool()
+        options = MockOptions(non_interactive=True)
+        step = Update(tool, options)
+        self.assertEqual(["mock-update-webkit"], step._update_command())
+
+        tool._deprecated_port = ChromiumPort()
+        self.assertEqual(["Tools/Scripts/update-webkit", "--chromium", "--force-update"], step._update_command())
+
+        tool._deprecated_port = ChromiumXVFBPort()
+        self.assertEqual(["Tools/Scripts/update-webkit", "--chromium", "--force-update"], step._update_command())
+
+        tool._deprecated_port = ChromiumAndroidPort()
+        self.assertEqual(["Tools/Scripts/update-webkit", "--chromium", "--force-update", "--chromium-android"], step._update_command())
+
+    def test_update_command_interactive(self):
+        tool = MockTool()
+        options = MockOptions(non_interactive=False)
+        step = Update(tool, options)
+        self.assertEqual(["mock-update-webkit"], step._update_command())
+
+        tool._deprecated_port = ChromiumPort()
+        self.assertEqual(["Tools/Scripts/update-webkit", "--chromium"], step._update_command())
+
+        tool._deprecated_port = ChromiumXVFBPort()
+        self.assertEqual(["Tools/Scripts/update-webkit", "--chromium"], step._update_command())
+
+        tool._deprecated_port = ChromiumAndroidPort()
+        self.assertEqual(["Tools/Scripts/update-webkit", "--chromium", "--chromium-android"], step._update_command())
diff --git a/Tools/Scripts/webkitpy/tool/steps/updatechangelogswithreview_unittest.py b/Tools/Scripts/webkitpy/tool/steps/updatechangelogswithreview_unittest.py
new file mode 100644
index 0000000..8ec8891
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/updatechangelogswithreview_unittest.py
@@ -0,0 +1,54 @@
+# Copyright (C) 2009 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.system.outputcapture import OutputCapture
+from webkitpy.tool.mocktool import MockOptions, MockTool
+from webkitpy.tool.steps.updatechangelogswithreviewer import UpdateChangeLogsWithReviewer
+
+class UpdateChangeLogsWithReviewerTest(unittest.TestCase):
+    def test_guess_reviewer_from_bug(self):
+        capture = OutputCapture()
+        step = UpdateChangeLogsWithReviewer(MockTool(), MockOptions())
+        expected_stderr = "No reviewed patches on bug 50001, cannot infer reviewer.\n"
+        capture.assert_outputs(self, step._guess_reviewer_from_bug, [50001], expected_stderr=expected_stderr)
+
+    def test_guess_reviewer_from_multipatch_bug(self):
+        capture = OutputCapture()
+        step = UpdateChangeLogsWithReviewer(MockTool(), MockOptions())
+        expected_stderr = "Guessing \"Reviewer2\" as reviewer from attachment 10001 on bug 50000.\n"
+        capture.assert_outputs(self, step._guess_reviewer_from_bug, [50000], expected_stderr=expected_stderr)
+
+    def test_empty_state(self):
+        capture = OutputCapture()
+        options = MockOptions()
+        options.reviewer = 'MOCK reviewer'
+        options.git_commit = 'MOCK git commit'
+        step = UpdateChangeLogsWithReviewer(MockTool(), options)
+        capture.assert_outputs(self, step.run, [{}])
diff --git a/Tools/Scripts/webkitpy/tool/steps/updatechangelogswithreviewer.py b/Tools/Scripts/webkitpy/tool/steps/updatechangelogswithreviewer.py
new file mode 100644
index 0000000..cc3e965
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/updatechangelogswithreviewer.py
@@ -0,0 +1,75 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.checkout.changelog import ChangeLog
+from webkitpy.tool.grammar import pluralize
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+from webkitpy.common.system.deprecated_logging import log, error
+
+class UpdateChangeLogsWithReviewer(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.git_commit,
+            Options.reviewer,
+        ]
+
+    def _guess_reviewer_from_bug(self, bug_id):
+        # FIXME: It's unclear if it would be safe to use self.cached_lookup(state, 'bug')
+        # here as we don't currently have a way to invalidate a bug after making changes (like ObsoletePatches does).
+        patches = self._tool.bugs.fetch_bug(bug_id).reviewed_patches()
+        if not patches:
+            log("%s on bug %s, cannot infer reviewer." % ("No reviewed patches", bug_id))
+            return None
+        patch = patches[-1]
+        log("Guessing \"%s\" as reviewer from attachment %s on bug %s." % (patch.reviewer().full_name, patch.id(), bug_id))
+        return patch.reviewer().full_name
+
+    def run(self, state):
+        bug_id = state.get("bug_id")
+        if not bug_id and state.get("patch"):
+            bug_id = state.get("patch").bug_id()
+
+        reviewer = self._options.reviewer
+        if not reviewer:
+            if not bug_id:
+                log("No bug id provided and --reviewer= not provided.  Not updating ChangeLogs with reviewer.")
+                return
+            reviewer = self._guess_reviewer_from_bug(bug_id)
+
+        if not reviewer:
+            log("Failed to guess reviewer from bug %s and --reviewer= not provided.  Not updating ChangeLogs with reviewer." % bug_id)
+            return
+
+        # cached_lookup("changelogs") is always absolute paths.
+        for changelog_path in self.cached_lookup(state, "changelogs"):
+            ChangeLog(changelog_path).set_reviewer(reviewer)
+
+        # Tell the world that we just changed something on disk so that the cached diff is invalidated.
+        self.did_modify_checkout(state)
diff --git a/Tools/Scripts/webkitpy/tool/steps/updatechromiumdeps.py b/Tools/Scripts/webkitpy/tool/steps/updatechromiumdeps.py
new file mode 100644
index 0000000..c9fc631
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/updatechromiumdeps.py
@@ -0,0 +1,73 @@
+# Copyright (C) 2011 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 urllib2
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+from webkitpy.common.config import urls
+from webkitpy.common.system.deprecated_logging import log, error
+
+
+class UpdateChromiumDEPS(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.non_interactive,
+        ]
+
+    # Notice that this method throws lots of exciting exceptions!
+    def _fetch_last_known_good_revision(self):
+        return int(urllib2.urlopen(urls.chromium_lkgr_url).read())
+
+    def _validate_revisions(self, current_chromium_revision, new_chromium_revision):
+        if new_chromium_revision < current_chromium_revision:
+            message = "Current Chromium DEPS revision %s is newer than %s." % (current_chromium_revision, new_chromium_revision)
+            if self._options.non_interactive:
+                error(message)  # Causes the process to terminate.
+            log(message)
+            new_chromium_revision = self._tool.user.prompt("Enter new chromium revision (enter nothing to cancel):\n")
+            try:
+                new_chromium_revision = int(new_chromium_revision)
+            except ValueError, TypeError:
+                new_chromium_revision = None
+            if not new_chromium_revision:
+                error("Unable to update Chromium DEPS")
+
+
+    def run(self, state):
+        # Note that state["chromium_revision"] must be defined, but can be None.
+        new_chromium_revision = state["chromium_revision"]
+        if not new_chromium_revision:
+            new_chromium_revision = self._fetch_last_known_good_revision()
+
+        deps = self._tool.checkout().chromium_deps()
+        current_chromium_revision = deps.read_variable("chromium_rev")
+        self._validate_revisions(current_chromium_revision, new_chromium_revision)
+        log("Updating Chromium DEPS to %s" % new_chromium_revision)
+        deps.write_variable("chromium_rev", new_chromium_revision)
diff --git a/Tools/Scripts/webkitpy/tool/steps/validatechangelogs.py b/Tools/Scripts/webkitpy/tool/steps/validatechangelogs.py
new file mode 100644
index 0000000..b6b33c0
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/validatechangelogs.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.
+
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+from webkitpy.common.checkout.diff_parser import DiffParser
+from webkitpy.common.system.deprecated_logging import error, log
+
+
+# This is closely related to the ValidateReviewer step and the CommitterValidator class.
+# We may want to unify more of this code in one place.
+class ValidateChangeLogs(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.non_interactive,
+        ]
+
+    def _check_changelog_diff(self, diff_file):
+        if not self._tool.checkout().is_path_to_changelog(diff_file.filename):
+            return True
+        # Each line is a tuple, the first value is the deleted line number
+        # Date, reviewer, bug title, bug url, and empty lines could all be
+        # identical in the most recent entries.  If the diff starts any
+        # later than that, assume that the entry is wrong.
+        if diff_file.lines[0][0] < 8:
+            return True
+        if self._options.non_interactive:
+            return False
+
+        log("The diff to %s looks wrong.  Are you sure your ChangeLog entry is at the top of the file?" % (diff_file.filename))
+        # FIXME: Do we need to make the file path absolute?
+        self._tool.scm().diff_for_file(diff_file.filename)
+        if self._tool.user.confirm("OK to continue?", default='n'):
+            return True
+        return False
+
+    def run(self, state):
+        changed_files = self.cached_lookup(state, "changed_files")
+        for filename in changed_files:
+            if not self._tool.checkout().is_path_to_changelog(filename):
+                continue
+            # Diff ChangeLogs directly because svn-create-patch will move
+            # ChangeLog entries to the # top automatically, defeating our
+            # validation here.
+            # FIXME: Should we diff all the ChangeLogs at once?
+            diff = self._tool.scm().diff_for_file(filename)
+            parsed_diff = DiffParser(diff.splitlines())
+            for filename, diff_file in parsed_diff.files.items():
+                if not self._check_changelog_diff(diff_file):
+                    error("ChangeLog entry in %s is not at the top of the file." % diff_file.filename)
diff --git a/Tools/Scripts/webkitpy/tool/steps/validatechangelogs_unittest.py b/Tools/Scripts/webkitpy/tool/steps/validatechangelogs_unittest.py
new file mode 100644
index 0000000..96bae9f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/validatechangelogs_unittest.py
@@ -0,0 +1,58 @@
+# 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.system.outputcapture import OutputCapture
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.mocktool import MockOptions, MockTool
+from webkitpy.tool.steps.validatechangelogs import ValidateChangeLogs
+
+
+class ValidateChangeLogsTest(unittest.TestCase):
+
+    def _assert_start_line_produces_output(self, start_line, should_fail=False, non_interactive=False):
+        tool = MockTool()
+        tool._checkout.is_path_to_changelog = lambda path: True
+        step = ValidateChangeLogs(tool, MockOptions(git_commit=None, non_interactive=non_interactive))
+        diff_file = Mock()
+        diff_file.filename = "mock/ChangeLog"
+        diff_file.lines = [(start_line, start_line, "foo")]
+        expected_stdout = expected_stderr = ""
+        if should_fail and not non_interactive:
+            expected_stderr = "The diff to mock/ChangeLog looks wrong.  Are you sure your ChangeLog entry is at the top of the file?\nOK to continue?\n"
+        result = OutputCapture().assert_outputs(self, step._check_changelog_diff, [diff_file], expected_stderr=expected_stderr)
+        self.assertEqual(not result, should_fail)
+
+    def test_check_changelog_diff(self):
+        self._assert_start_line_produces_output(1)
+        self._assert_start_line_produces_output(7)
+        self._assert_start_line_produces_output(8, should_fail=True)
+
+        self._assert_start_line_produces_output(1, non_interactive=False)
+        self._assert_start_line_produces_output(8, non_interactive=True, should_fail=True)
diff --git a/Tools/Scripts/webkitpy/tool/steps/validatereviewer.py b/Tools/Scripts/webkitpy/tool/steps/validatereviewer.py
new file mode 100644
index 0000000..5e93821
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/steps/validatereviewer.py
@@ -0,0 +1,55 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.checkout.changelog import ChangeLog
+from webkitpy.tool.steps.abstractstep import AbstractStep
+from webkitpy.tool.steps.options import Options
+from webkitpy.common.system.deprecated_logging import error, log
+
+
+# FIXME: Some of this logic should probably be unified with CommitterValidator?
+class ValidateReviewer(AbstractStep):
+    @classmethod
+    def options(cls):
+        return AbstractStep.options() + [
+            Options.non_interactive,
+        ]
+
+    def run(self, state):
+        # FIXME: For now we disable this check when a user is driving the script
+        # this check is too draconian (and too poorly tested) to foist upon users.
+        if not self._options.non_interactive:
+            return
+        for changelog_path in self.cached_lookup(state, "changelogs"):
+            changelog_entry = ChangeLog(changelog_path).latest_entry()
+            if changelog_entry.has_valid_reviewer():
+                continue
+            reviewer_text = changelog_entry.reviewer_text()
+            if reviewer_text:
+                log("%s found in %s does not appear to be a valid reviewer according to committers.py." % (reviewer_text, changelog_path))
+            error('%s neither lists a valid reviewer nor contains the string "Unreviewed" or "Rubber stamp" (case insensitive).' % changelog_path)
diff --git a/Tools/Scripts/webkitpy/webkitpy.pyproj b/Tools/Scripts/webkitpy/webkitpy.pyproj
new file mode 100644
index 0000000..0bff5fc
--- /dev/null
+++ b/Tools/Scripts/webkitpy/webkitpy.pyproj
@@ -0,0 +1,540 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>{59b0a791-93fe-40f8-a52b-ba19b73e8fa6}</ProjectGuid>
+    <ProjectHome>.</ProjectHome>
+    <StartupFile>layout_tests\run_webkit_tests.py</StartupFile>
+    <SearchPath>
+    </SearchPath>
+    <WorkingDirectory>../</WorkingDirectory>
+    <OutputPath>.</OutputPath>
+    <Name>webkitpy</Name>
+    <RootNamespace>webkitpy</RootNamespace>
+    <IsWindowsApplication>False</IsWindowsApplication>
+    <LaunchProvider>Standard Python launcher</LaunchProvider>
+    <CommandLineArguments>--platform=mock --no-pixel-tests --no-retry-failures</CommandLineArguments>
+    <InterpreterPath />
+    <InterpreterArguments />
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <DebugSymbols>true</DebugSymbols>
+    <EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+    <DebugSymbols>true</DebugSymbols>
+    <EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
+  </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="bindings\main.py" />
+    <Compile Include="bindings\__init__.py" />
+    <Compile Include="common\checkout\baselineoptimizer.py" />
+    <Compile Include="common\checkout\baselineoptimizer_unittest.py" />
+    <Compile Include="common\checkout\changelog.py" />
+    <Compile Include="common\checkout\changelog_unittest.py" />
+    <Compile Include="common\checkout\checkout.py" />
+    <Compile Include="common\checkout\checkout_mock.py" />
+    <Compile Include="common\checkout\checkout_unittest.py" />
+    <Compile Include="common\checkout\commitinfo.py" />
+    <Compile Include="common\checkout\commitinfo_unittest.py" />
+    <Compile Include="common\checkout\deps.py" />
+    <Compile Include="common\checkout\deps_mock.py" />
+    <Compile Include="common\checkout\diff_parser.py" />
+    <Compile Include="common\checkout\diff_parser_unittest.py" />
+    <Compile Include="common\checkout\diff_test_data.py" />
+    <Compile Include="common\checkout\scm\commitmessage.py" />
+    <Compile Include="common\checkout\scm\detection.py" />
+    <Compile Include="common\checkout\scm\detection_unittest.py" />
+    <Compile Include="common\checkout\scm\git.py" />
+    <Compile Include="common\checkout\scm\scm.py" />
+    <Compile Include="common\checkout\scm\scm_mock.py" />
+    <Compile Include="common\checkout\scm\scm_unittest.py" />
+    <Compile Include="common\checkout\scm\svn.py" />
+    <Compile Include="common\checkout\scm\__init__.py" />
+    <Compile Include="common\checkout\__init__.py" />
+    <Compile Include="common\checksvnconfigfile.py" />
+    <Compile Include="common\config\build.py" />
+    <Compile Include="common\config\build_unittest.py" />
+    <Compile Include="common\config\committers.py" />
+    <Compile Include="common\config\committers_unittest.py" />
+    <Compile Include="common\config\committervalidator.py" />
+    <Compile Include="common\config\committervalidator_unittest.py" />
+    <Compile Include="common\config\contributionareas.py" />
+    <Compile Include="common\config\contributionareas_unittest.py" />
+    <Compile Include="common\config\irc.py" />
+    <Compile Include="common\config\ports.py" />
+    <Compile Include="common\config\ports_mock.py" />
+    <Compile Include="common\config\ports_unittest.py" />
+    <Compile Include="common\config\urls.py" />
+    <Compile Include="common\config\urls_unittest.py" />
+    <Compile Include="common\config\__init__.py" />
+    <Compile Include="common\editdistance.py" />
+    <Compile Include="common\editdistance_unittest.py" />
+    <Compile Include="common\find_files.py" />
+    <Compile Include="common\find_files_unittest.py" />
+    <Compile Include="common\host.py" />
+    <Compile Include="common\host_mock.py" />
+    <Compile Include="common\lru_cache.py" />
+    <Compile Include="common\lru_cache_unittest.py" />
+    <Compile Include="common\memoized.py" />
+    <Compile Include="common\memoized_unittest.py" />
+    <Compile Include="common\message_pool.py" />
+    <Compile Include="common\net\bugzilla\attachment.py" />
+    <Compile Include="common\net\bugzilla\bug.py" />
+    <Compile Include="common\net\bugzilla\bugzilla.py" />
+    <Compile Include="common\net\bugzilla\bugzilla_mock.py" />
+    <Compile Include="common\net\bugzilla\bugzilla_unittest.py" />
+    <Compile Include="common\net\bugzilla\bug_unittest.py" />
+    <Compile Include="common\net\bugzilla\__init__.py" />
+    <Compile Include="common\net\buildbot\buildbot.py" />
+    <Compile Include="common\net\buildbot\buildbot_mock.py" />
+    <Compile Include="common\net\buildbot\buildbot_unittest.py" />
+    <Compile Include="common\net\buildbot\chromiumbuildbot.py" />
+    <Compile Include="common\net\buildbot\__init__.py" />
+    <Compile Include="common\net\credentials.py" />
+    <Compile Include="common\net\credentials_unittest.py" />
+    <Compile Include="common\net\failuremap.py" />
+    <Compile Include="common\net\failuremap_unittest.py" />
+    <Compile Include="common\net\file_uploader.py" />
+    <Compile Include="common\net\htdigestparser.py" />
+    <Compile Include="common\net\htdigestparser_unittest.py" />
+    <Compile Include="common\net\irc\ircbot.py" />
+    <Compile Include="common\net\irc\ircproxy.py" />
+    <Compile Include="common\net\irc\ircproxy_unittest.py" />
+    <Compile Include="common\net\irc\irc_mock.py" />
+    <Compile Include="common\net\irc\__init__.py" />
+    <Compile Include="common\net\layouttestresults.py" />
+    <Compile Include="common\net\layouttestresults_unittest.py" />
+    <Compile Include="common\net\networktransaction.py" />
+    <Compile Include="common\net\networktransaction_unittest.py" />
+    <Compile Include="common\net\omahaproxy.py" />
+    <Compile Include="common\net\omahaproxy_unittest.py" />
+    <Compile Include="common\net\regressionwindow.py" />
+    <Compile Include="common\net\resultsjsonparser.py" />
+    <Compile Include="common\net\resultsjsonparser_unittest.py" />
+    <Compile Include="common\net\statusserver.py" />
+    <Compile Include="common\net\statusserver_mock.py" />
+    <Compile Include="common\net\statusserver_unittest.py" />
+    <Compile Include="common\net\unittestresults.py" />
+    <Compile Include="common\net\unittestresults_unittest.py" />
+    <Compile Include="common\net\web.py" />
+    <Compile Include="common\net\web_mock.py" />
+    <Compile Include="common\net\__init__.py" />
+    <Compile Include="common\newstringio.py" />
+    <Compile Include="common\newstringio_unittest.py" />
+    <Compile Include="common\prettypatch.py" />
+    <Compile Include="common\prettypatch_unittest.py" />
+    <Compile Include="common\read_checksum_from_png.py" />
+    <Compile Include="common\read_checksum_from_png_unittest.py" />
+    <Compile Include="common\system\autoinstall.py" />
+    <Compile Include="common\system\crashlogs.py" />
+    <Compile Include="common\system\crashlogs_unittest.py" />
+    <Compile Include="common\system\deprecated_logging.py" />
+    <Compile Include="common\system\deprecated_logging_unittest.py" />
+    <Compile Include="common\system\environment.py" />
+    <Compile Include="common\system\environment_unittest.py" />
+    <Compile Include="common\system\executive.py" />
+    <Compile Include="common\system\executive_mock.py" />
+    <Compile Include="common\system\executive_unittest.py" />
+    <Compile Include="common\system\fileset.py" />
+    <Compile Include="common\system\filesystem.py" />
+    <Compile Include="common\system\filesystem_mock.py" />
+    <Compile Include="common\system\filesystem_mock_unittest.py" />
+    <Compile Include="common\system\filesystem_unittest.py" />
+    <Compile Include="common\system\file_lock.py" />
+    <Compile Include="common\system\file_lock_integrationtest.py" />
+    <Compile Include="common\system\logtesting.py" />
+    <Compile Include="common\system\logutils.py" />
+    <Compile Include="common\system\logutils_unittest.py" />
+    <Compile Include="common\system\outputcapture.py" />
+    <Compile Include="common\system\outputcapture_unittest.py" />
+    <Compile Include="common\system\path.py" />
+    <Compile Include="common\system\path_unittest.py" />
+    <Compile Include="common\system\platforminfo.py" />
+    <Compile Include="common\system\platforminfo_mock.py" />
+    <Compile Include="common\system\platforminfo_unittest.py" />
+    <Compile Include="common\system\stack_utils.py" />
+    <Compile Include="common\system\stack_utils_unittest.py" />
+    <Compile Include="common\system\systemhost.py" />
+    <Compile Include="common\system\systemhost_mock.py" />
+    <Compile Include="common\system\urlfetcher.py" />
+    <Compile Include="common\system\urlfetcher_mock.py" />
+    <Compile Include="common\system\user.py" />
+    <Compile Include="common\system\user_mock.py" />
+    <Compile Include="common\system\user_unittest.py" />
+    <Compile Include="common\system\workspace.py" />
+    <Compile Include="common\system\workspace_mock.py" />
+    <Compile Include="common\system\workspace_unittest.py" />
+    <Compile Include="common\system\zipfileset.py" />
+    <Compile Include="common\system\zipfileset_mock.py" />
+    <Compile Include="common\system\zipfileset_unittest.py" />
+    <Compile Include="common\system\zip_mock.py" />
+    <Compile Include="common\system\__init__.py" />
+    <Compile Include="common\thread\messagepump.py" />
+    <Compile Include="common\thread\messagepump_unittest.py" />
+    <Compile Include="common\thread\threadedmessagequeue.py" />
+    <Compile Include="common\thread\threadedmessagequeue_unittest.py" />
+    <Compile Include="common\thread\__init__.py" />
+    <Compile Include="common\version_check.py" />
+    <Compile Include="common\watchlist\amountchangedpattern.py" />
+    <Compile Include="common\watchlist\amountchangedpattern_unittest.py" />
+    <Compile Include="common\watchlist\changedlinepattern.py" />
+    <Compile Include="common\watchlist\changedlinepattern_unittest.py" />
+    <Compile Include="common\watchlist\filenamepattern.py" />
+    <Compile Include="common\watchlist\filenamepattern_unittest.py" />
+    <Compile Include="common\watchlist\watchlist.py" />
+    <Compile Include="common\watchlist\watchlistloader.py" />
+    <Compile Include="common\watchlist\watchlistloader_unittest.py" />
+    <Compile Include="common\watchlist\watchlistparser.py" />
+    <Compile Include="common\watchlist\watchlistparser_unittest.py" />
+    <Compile Include="common\watchlist\watchlistrule.py" />
+    <Compile Include="common\watchlist\watchlistrule_unittest.py" />
+    <Compile Include="common\watchlist\watchlist_mock.py" />
+    <Compile Include="common\watchlist\watchlist_unittest.py" />
+    <Compile Include="common\watchlist\__init__.py" />
+    <Compile Include="common\webkitunittest.py" />
+    <Compile Include="common\__init__.py" />
+    <Compile Include="layout_tests\controllers\manager.py" />
+    <Compile Include="layout_tests\controllers\manager_unittest.py" />
+    <Compile Include="layout_tests\controllers\single_test_runner.py" />
+    <Compile Include="layout_tests\controllers\test_expectations_editor.py" />
+    <Compile Include="layout_tests\controllers\test_expectations_editor_unittest.py" />
+    <Compile Include="layout_tests\controllers\test_result_writer.py" />
+    <Compile Include="layout_tests\controllers\test_result_writer_unittest.py" />
+    <Compile Include="layout_tests\controllers\worker.py" />
+    <Compile Include="layout_tests\controllers\__init__.py" />
+    <Compile Include="layout_tests\layout_package\json_layout_results_generator.py" />
+    <Compile Include="layout_tests\layout_package\json_results_generator.py" />
+    <Compile Include="layout_tests\layout_package\json_results_generator_unittest.py" />
+    <Compile Include="layout_tests\layout_package\__init__.py" />
+    <Compile Include="layout_tests\models\result_summary.py" />
+    <Compile Include="layout_tests\models\test_configuration.py" />
+    <Compile Include="layout_tests\models\test_configuration_unittest.py" />
+    <Compile Include="layout_tests\models\test_expectations.py" />
+    <Compile Include="layout_tests\models\test_expectations_unittest.py" />
+    <Compile Include="layout_tests\models\test_failures.py" />
+    <Compile Include="layout_tests\models\test_failures_unittest.py" />
+    <Compile Include="layout_tests\models\test_input.py" />
+    <Compile Include="layout_tests\models\test_results.py" />
+    <Compile Include="layout_tests\models\test_results_unittest.py" />
+    <Compile Include="layout_tests\models\__init__.py" />
+    <Compile Include="layout_tests\port\apple.py" />
+    <Compile Include="layout_tests\port\base.py" />
+    <Compile Include="layout_tests\port\base_unittest.py" />
+    <Compile Include="layout_tests\port\builders.py" />
+    <Compile Include="layout_tests\port\builders_unittest.py" />
+    <Compile Include="layout_tests\port\chromium.py" />
+    <Compile Include="layout_tests\port\chromium_android.py" />
+    <Compile Include="layout_tests\port\chromium_android_unittest.py" />
+    <Compile Include="layout_tests\port\chromium_linux.py" />
+    <Compile Include="layout_tests\port\chromium_linux_unittest.py" />
+    <Compile Include="layout_tests\port\chromium_mac.py" />
+    <Compile Include="layout_tests\port\chromium_mac_unittest.py" />
+    <Compile Include="layout_tests\port\chromium_port_testcase.py" />
+    <Compile Include="layout_tests\port\chromium_unittest.py" />
+    <Compile Include="layout_tests\port\chromium_win.py" />
+    <Compile Include="layout_tests\port\chromium_win_unittest.py" />
+    <Compile Include="layout_tests\port\config.py" />
+    <Compile Include="layout_tests\port\config_mock.py" />
+    <Compile Include="layout_tests\port\config_standalone.py" />
+    <Compile Include="layout_tests\port\config_unittest.py" />
+    <Compile Include="layout_tests\port\driver.py" />
+    <Compile Include="layout_tests\port\driver_unittest.py" />
+    <Compile Include="layout_tests\port\efl.py" />
+    <Compile Include="layout_tests\port\efl_unittest.py" />
+    <Compile Include="layout_tests\port\factory.py" />
+    <Compile Include="layout_tests\port\factory_unittest.py" />
+    <Compile Include="layout_tests\port\gtk.py" />
+    <Compile Include="layout_tests\port\gtk_unittest.py" />
+    <Compile Include="layout_tests\port\http_lock.py" />
+    <Compile Include="layout_tests\port\http_lock_unittest.py" />
+    <Compile Include="layout_tests\port\leakdetector.py" />
+    <Compile Include="layout_tests\port\leakdetector_unittest.py" />
+    <Compile Include="layout_tests\port\mac.py" />
+    <Compile Include="layout_tests\port\mac_unittest.py" />
+    <Compile Include="layout_tests\port\mock_drt.py" />
+    <Compile Include="layout_tests\port\mock_drt_unittest.py" />
+    <Compile Include="layout_tests\port\port_testcase.py" />
+    <Compile Include="layout_tests\port\pulseaudio_sanitizer.py" />
+    <Compile Include="layout_tests\port\qt.py" />
+    <Compile Include="layout_tests\port\qt_unittest.py" />
+    <Compile Include="layout_tests\port\server_process.py" />
+    <Compile Include="layout_tests\port\server_process_unittest.py" />
+    <Compile Include="layout_tests\port\test.py" />
+    <Compile Include="layout_tests\port\webkit.py" />
+    <Compile Include="layout_tests\port\webkit_unittest.py" />
+    <Compile Include="layout_tests\port\win.py" />
+    <Compile Include="layout_tests\port\win_unittest.py" />
+    <Compile Include="layout_tests\port\xvfbdriver.py" />
+    <Compile Include="layout_tests\port\__init__.py" />
+    <Compile Include="layout_tests\reftests\extract_reference_link.py" />
+    <Compile Include="layout_tests\reftests\extract_reference_link_unittest.py" />
+    <Compile Include="layout_tests\reftests\__init__.py" />
+    <Compile Include="layout_tests\run_webkit_tests.py" />
+    <Compile Include="layout_tests\run_webkit_tests_integrationtest.py" />
+    <Compile Include="layout_tests\servers\apache_http_server.py" />
+    <Compile Include="layout_tests\servers\apache_http_server_unittest.py" />
+    <Compile Include="layout_tests\servers\http_server.py" />
+    <Compile Include="layout_tests\servers\http_server_base.py" />
+    <Compile Include="layout_tests\servers\http_server_integrationtest.py" />
+    <Compile Include="layout_tests\servers\http_server_unittest.py" />
+    <Compile Include="layout_tests\servers\websocket_server.py" />
+    <Compile Include="layout_tests\servers\__init__.py" />
+    <Compile Include="layout_tests\views\metered_stream.py" />
+    <Compile Include="layout_tests\views\metered_stream_unittest.py" />
+    <Compile Include="layout_tests\views\printing.py" />
+    <Compile Include="layout_tests\views\printing_unittest.py" />
+    <Compile Include="layout_tests\views\__init__.py" />
+    <Compile Include="layout_tests\__init__.py" />
+    <Compile Include="performance_tests\perftest.py" />
+    <Compile Include="performance_tests\perftestsrunner.py" />
+    <Compile Include="performance_tests\perftestsrunner_unittest.py" />
+    <Compile Include="performance_tests\perftest_unittest.py" />
+    <Compile Include="performance_tests\__init__.py" />
+    <Compile Include="style\checker.py" />
+    <Compile Include="style\checkers\changelog.py" />
+    <Compile Include="style\checkers\changelog_unittest.py" />
+    <Compile Include="style\checkers\common.py" />
+    <Compile Include="style\checkers\common_unittest.py" />
+    <Compile Include="style\checkers\cpp.py" />
+    <Compile Include="style\checkers\cpp_unittest.py" />
+    <Compile Include="style\checkers\jsonchecker.py" />
+    <Compile Include="style\checkers\jsonchecker_unittest.py" />
+    <Compile Include="style\checkers\png.py" />
+    <Compile Include="style\checkers\png_unittest.py" />
+    <Compile Include="style\checkers\python.py" />
+    <Compile Include="style\checkers\python_unittest.py" />
+    <Compile Include="style\checkers\python_unittest_input.py" />
+    <Compile Include="style\checkers\test_expectations.py" />
+    <Compile Include="style\checkers\test_expectations_unittest.py" />
+    <Compile Include="style\checkers\text.py" />
+    <Compile Include="style\checkers\text_unittest.py" />
+    <Compile Include="style\checkers\watchlist.py" />
+    <Compile Include="style\checkers\watchlist_unittest.py" />
+    <Compile Include="style\checkers\xcodeproj.py" />
+    <Compile Include="style\checkers\xcodeproj_unittest.py" />
+    <Compile Include="style\checkers\xml.py" />
+    <Compile Include="style\checkers\xml_unittest.py" />
+    <Compile Include="style\checkers\__init__.py" />
+    <Compile Include="style\checker_unittest.py" />
+    <Compile Include="style\error_handlers.py" />
+    <Compile Include="style\error_handlers_unittest.py" />
+    <Compile Include="style\filereader.py" />
+    <Compile Include="style\filereader_unittest.py" />
+    <Compile Include="style\filter.py" />
+    <Compile Include="style\filter_unittest.py" />
+    <Compile Include="style\main.py" />
+    <Compile Include="style\main_unittest.py" />
+    <Compile Include="style\optparser.py" />
+    <Compile Include="style\optparser_unittest.py" />
+    <Compile Include="style\patchreader.py" />
+    <Compile Include="style\patchreader_unittest.py" />
+    <Compile Include="style\__init__.py" />
+    <Compile Include="test\finder.py" />
+    <Compile Include="test\finder_unittest.py" />
+    <Compile Include="test\main.py" />
+    <Compile Include="test\main_unittest.py" />
+    <Compile Include="test\printer.py" />
+    <Compile Include="test\runner.py" />
+    <Compile Include="test\runner_unittest.py" />
+    <Compile Include="test\skip.py" />
+    <Compile Include="test\skip_unittest.py" />
+    <Compile Include="test\__init__.py" />
+    <Compile Include="thirdparty\BeautifulSoup.py" />
+    <Compile Include="thirdparty\mock.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\common.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\dispatch.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\extensions.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\handshake\draft75.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\handshake\hybi.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\handshake\hybi00.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\handshake\_base.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\handshake\__init__.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\headerparserhandler.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\http_header_util.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\memorizingfile.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\msgutil.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\standalone.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\stream.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\util.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\_stream_base.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\_stream_hixie75.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\_stream_hybi.py" />
+    <Compile Include="thirdparty\mod_pywebsocket\__init__.py" />
+    <Compile Include="thirdparty\ordered_dict.py" />
+    <Compile Include="thirdparty\__init__.py" />
+    <Compile Include="thirdparty\__init___unittest.py" />
+    <Compile Include="tool\bot\botinfo.py" />
+    <Compile Include="tool\bot\botinfo_unittest.py" />
+    <Compile Include="tool\bot\commitqueuetask.py" />
+    <Compile Include="tool\bot\commitqueuetask_unittest.py" />
+    <Compile Include="tool\bot\earlywarningsystemtask.py" />
+    <Compile Include="tool\bot\expectedfailures.py" />
+    <Compile Include="tool\bot\expectedfailures_unittest.py" />
+    <Compile Include="tool\bot\feeders.py" />
+    <Compile Include="tool\bot\feeders_unittest.py" />
+    <Compile Include="tool\bot\flakytestreporter.py" />
+    <Compile Include="tool\bot\flakytestreporter_unittest.py" />
+    <Compile Include="tool\bot\irc_command.py" />
+    <Compile Include="tool\bot\irc_command_unittest.py" />
+    <Compile Include="tool\bot\ircbot.py" />
+    <Compile Include="tool\bot\ircbot_unittest.py" />
+    <Compile Include="tool\bot\layouttestresultsreader.py" />
+    <Compile Include="tool\bot\layouttestresultsreader_unittest.py" />
+    <Compile Include="tool\bot\patchanalysistask.py" />
+    <Compile Include="tool\bot\queueengine.py" />
+    <Compile Include="tool\bot\queueengine_unittest.py" />
+    <Compile Include="tool\bot\sheriff.py" />
+    <Compile Include="tool\bot\sheriff_unittest.py" />
+    <Compile Include="tool\bot\stylequeuetask.py" />
+    <Compile Include="tool\bot\__init__.py" />
+    <Compile Include="tool\commands\abstractlocalservercommand.py" />
+    <Compile Include="tool\commands\abstractsequencedcommand.py" />
+    <Compile Include="tool\commands\adduserstogroups.py" />
+    <Compile Include="tool\commands\analyzechangelog.py" />
+    <Compile Include="tool\commands\analyzechangelog_unittest.py" />
+    <Compile Include="tool\commands\applywatchlistlocal.py" />
+    <Compile Include="tool\commands\applywatchlistlocal_unittest.py" />
+    <Compile Include="tool\commands\bugfortest.py" />
+    <Compile Include="tool\commands\bugsearch.py" />
+    <Compile Include="tool\commands\chromechannels.py" />
+    <Compile Include="tool\commands\chromechannels_unittest.py" />
+    <Compile Include="tool\commands\commandtest.py" />
+    <Compile Include="tool\commands\download.py" />
+    <Compile Include="tool\commands\download_unittest.py" />
+    <Compile Include="tool\commands\earlywarningsystem.py" />
+    <Compile Include="tool\commands\earlywarningsystem_unittest.py" />
+    <Compile Include="tool\commands\expectations.py" />
+    <Compile Include="tool\commands\findusers.py" />
+    <Compile Include="tool\commands\gardenomatic.py" />
+    <Compile Include="tool\commands\openbugs.py" />
+    <Compile Include="tool\commands\openbugs_unittest.py" />
+    <Compile Include="tool\commands\prettydiff.py" />
+    <Compile Include="tool\commands\queries.py" />
+    <Compile Include="tool\commands\queries_unittest.py" />
+    <Compile Include="tool\commands\queues.py" />
+    <Compile Include="tool\commands\queuestest.py" />
+    <Compile Include="tool\commands\queues_unittest.py" />
+    <Compile Include="tool\commands\rebaseline.py" />
+    <Compile Include="tool\commands\rebaselineserver.py" />
+    <Compile Include="tool\commands\rebaseline_unittest.py" />
+    <Compile Include="tool\commands\roll.py" />
+    <Compile Include="tool\commands\roll_unittest.py" />
+    <Compile Include="tool\commands\sheriffbot.py" />
+    <Compile Include="tool\commands\sheriffbot_unittest.py" />
+    <Compile Include="tool\commands\stepsequence.py" />
+    <Compile Include="tool\commands\suggestnominations.py" />
+    <Compile Include="tool\commands\suggestnominations_unittest.py" />
+    <Compile Include="tool\commands\upload.py" />
+    <Compile Include="tool\commands\upload_unittest.py" />
+    <Compile Include="tool\commands\__init__.py" />
+    <Compile Include="tool\comments.py" />
+    <Compile Include="tool\grammar.py" />
+    <Compile Include="tool\grammar_unittest.py" />
+    <Compile Include="tool\main.py" />
+    <Compile Include="tool\mocktool.py" />
+    <Compile Include="tool\mocktool_unittest.py" />
+    <Compile Include="tool\multicommandtool.py" />
+    <Compile Include="tool\multicommandtool_unittest.py" />
+    <Compile Include="tool\servers\gardeningserver.py" />
+    <Compile Include="tool\servers\gardeningserver_unittest.py" />
+    <Compile Include="tool\servers\rebaselineserver.py" />
+    <Compile Include="tool\servers\rebaselineserver_unittest.py" />
+    <Compile Include="tool\servers\reflectionhandler.py" />
+    <Compile Include="tool\servers\__init__.py" />
+    <Compile Include="tool\steps\abstractstep.py" />
+    <Compile Include="tool\steps\addsvnmimetypeforpng.py" />
+    <Compile Include="tool\steps\addsvnmimetypeforpng_unittest.py" />
+    <Compile Include="tool\steps\applypatch.py" />
+    <Compile Include="tool\steps\applypatchwithlocalcommit.py" />
+    <Compile Include="tool\steps\applywatchlist.py" />
+    <Compile Include="tool\steps\applywatchlist_unittest.py" />
+    <Compile Include="tool\steps\attachtobug.py" />
+    <Compile Include="tool\steps\build.py" />
+    <Compile Include="tool\steps\checkstyle.py" />
+    <Compile Include="tool\steps\cleanworkingdirectory.py" />
+    <Compile Include="tool\steps\cleanworkingdirectorywithlocalcommits.py" />
+    <Compile Include="tool\steps\cleanworkingdirectory_unittest.py" />
+    <Compile Include="tool\steps\closebug.py" />
+    <Compile Include="tool\steps\closebugforlanddiff.py" />
+    <Compile Include="tool\steps\closebugforlanddiff_unittest.py" />
+    <Compile Include="tool\steps\closepatch.py" />
+    <Compile Include="tool\steps\commit.py" />
+    <Compile Include="tool\steps\commit_unittest.py" />
+    <Compile Include="tool\steps\confirmdiff.py" />
+    <Compile Include="tool\steps\createbug.py" />
+    <Compile Include="tool\steps\editchangelog.py" />
+    <Compile Include="tool\steps\ensurebugisopenandassigned.py" />
+    <Compile Include="tool\steps\ensurelocalcommitifneeded.py" />
+    <Compile Include="tool\steps\metastep.py" />
+    <Compile Include="tool\steps\obsoletepatches.py" />
+    <Compile Include="tool\steps\options.py" />
+    <Compile Include="tool\steps\postdiff.py" />
+    <Compile Include="tool\steps\postdiffforcommit.py" />
+    <Compile Include="tool\steps\postdiffforrevert.py" />
+    <Compile Include="tool\steps\preparechangelog.py" />
+    <Compile Include="tool\steps\preparechangelogfordepsroll.py" />
+    <Compile Include="tool\steps\preparechangelogforrevert.py" />
+    <Compile Include="tool\steps\preparechangelogforrevert_unittest.py" />
+    <Compile Include="tool\steps\preparechangelog_unittest.py" />
+    <Compile Include="tool\steps\promptforbugortitle.py" />
+    <Compile Include="tool\steps\reopenbugafterrollout.py" />
+    <Compile Include="tool\steps\revertrevision.py" />
+    <Compile Include="tool\steps\runtests.py" />
+    <Compile Include="tool\steps\runtests_unittest.py" />
+    <Compile Include="tool\steps\steps_unittest.py" />
+    <Compile Include="tool\steps\suggestreviewers.py" />
+    <Compile Include="tool\steps\suggestreviewers_unittest.py" />
+    <Compile Include="tool\steps\update.py" />
+    <Compile Include="tool\steps\updatechangelogswithreviewer.py" />
+    <Compile Include="tool\steps\updatechangelogswithreview_unittest.py" />
+    <Compile Include="tool\steps\updatechromiumdeps.py" />
+    <Compile Include="tool\steps\update_unittest.py" />
+    <Compile Include="tool\steps\validatechangelogs.py" />
+    <Compile Include="tool\steps\validatechangelogs_unittest.py" />
+    <Compile Include="tool\steps\validatereviewer.py" />
+    <Compile Include="tool\steps\__init__.py" />
+    <Compile Include="tool\__init__.py" />
+    <Compile Include="to_be_moved\update_webgl_conformance_tests.py" />
+    <Compile Include="to_be_moved\update_webgl_conformance_tests_unittest.py" />
+    <Compile Include="to_be_moved\__init__.py" />
+    <Compile Include="__init__.py" />
+  </ItemGroup>
+  <ItemGroup>
+    <Folder Include="bindings\" />
+    <Folder Include="common\" />
+    <Folder Include="common\checkout\" />
+    <Folder Include="common\checkout\scm\" />
+    <Folder Include="common\config\" />
+    <Folder Include="common\net\" />
+    <Folder Include="common\net\bugzilla\" />
+    <Folder Include="common\net\buildbot\" />
+    <Folder Include="common\net\irc\" />
+    <Folder Include="common\system\" />
+    <Folder Include="common\thread\" />
+    <Folder Include="common\watchlist\" />
+    <Folder Include="layout_tests\" />
+    <Folder Include="layout_tests\controllers\" />
+    <Folder Include="layout_tests\layout_package\" />
+    <Folder Include="layout_tests\models\" />
+    <Folder Include="layout_tests\port\" />
+    <Folder Include="layout_tests\reftests\" />
+    <Folder Include="layout_tests\servers\" />
+    <Folder Include="layout_tests\views\" />
+    <Folder Include="performance_tests\" />
+    <Folder Include="style\" />
+    <Folder Include="style\checkers\" />
+    <Folder Include="test\" />
+    <Folder Include="thirdparty\" />
+    <Folder Include="thirdparty\mod_pywebsocket\" />
+    <Folder Include="thirdparty\mod_pywebsocket\handshake\" />
+    <Folder Include="tool\" />
+    <Folder Include="tool\bot\" />
+    <Folder Include="tool\commands\" />
+    <Folder Include="tool\servers\" />
+    <Folder Include="tool\steps\" />
+    <Folder Include="to_be_moved\" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.Common.targets" />
+</Project>
\ No newline at end of file
diff --git a/Tools/Scripts/webkitpy/webkitpy.sln b/Tools/Scripts/webkitpy/webkitpy.sln
new file mode 100644
index 0000000..7648387
--- /dev/null
+++ b/Tools/Scripts/webkitpy/webkitpy.sln
@@ -0,0 +1,18 @@
+
+Microsoft Visual Studio Solution File, Format Version 11.00
+# Visual Studio 2010
+Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "webkitpy", "webkitpy.pyproj", "{59B0A791-93FE-40F8-A52B-BA19B73E8FA6}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{59B0A791-93FE-40F8-A52B-BA19B73E8FA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{59B0A791-93FE-40F8-A52B-BA19B73E8FA6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+EndGlobal