c0db3325dd29386a51dc8e8667dba223fd273b84
[WebKit-https.git] / WebKitTools / Scripts / run-webkit-tests
1 #!/usr/bin/perl
2
3 # Copyright (C) 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
4 # Copyright (C) 2006 Alexey Proskuryakov (ap@nypop.com)
5 # Copyright (C) 2007 Matt Lilek (pewtermoose@gmail.com)
6 # Copyright (C) 2007 Eric Seidel <eric@webkit.org>
7 # Copyright (C) 2009 Google Inc. All rights reserved.
8 # Copyright (C) 2009 Andras Becsi (becsi.andras@stud.u-szeged.hu), University of Szeged
9 #
10 # Redistribution and use in source and binary forms, with or without
11 # modification, are permitted provided that the following conditions
12 # are met:
13 #
14 # 1.  Redistributions of source code must retain the above copyright
15 #     notice, this list of conditions and the following disclaimer. 
16 # 2.  Redistributions in binary form must reproduce the above copyright
17 #     notice, this list of conditions and the following disclaimer in the
18 #     documentation and/or other materials provided with the distribution. 
19 # 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
20 #     its contributors may be used to endorse or promote products derived
21 #     from this software without specific prior written permission. 
22 #
23 # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
24 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
25 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26 # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
27 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
28 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
30 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
31 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
32 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33
34 # Script to run the WebKit Open Source Project layout tests.
35
36 # Run all the tests passed in on the command line.
37 # If no tests are passed, find all the .html, .shtml, .xml, .xhtml, .pl, .php (and svg) files in the test directory.
38
39 # Run each text.
40 # Compare against the existing file xxx-expected.txt.
41 # If there is a mismatch, generate xxx-actual.txt and xxx-diffs.txt.
42
43 # At the end, report:
44 #   the number of tests that got the expected results
45 #   the number of tests that ran, but did not get the expected results
46 #   the number of tests that failed to run
47 #   the number of tests that were run but had no expected results to compare against
48
49 use strict;
50 use warnings;
51
52 use Cwd;
53 use Data::Dumper;
54 use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK);
55 use File::Basename;
56 use File::Copy;
57 use File::Find;
58 use File::Path;
59 use File::Spec;
60 use File::Spec::Functions;
61 use FindBin;
62 use Getopt::Long;
63 use IPC::Open2;
64 use IPC::Open3;
65 use Time::HiRes qw(time usleep);
66
67 use List::Util 'shuffle';
68
69 use lib $FindBin::Bin;
70 use webkitdirs;
71 use VCSUtils;
72 use POSIX;
73
74 sub buildPlatformResultHierarchy();
75 sub buildPlatformTestHierarchy(@);
76 sub closeCygpaths();
77 sub closeDumpTool();
78 sub closeHTTPD();
79 sub closeWebSocketServer();
80 sub countAndPrintLeaks($$$);
81 sub countFinishedTest($$$$);
82 sub deleteExpectedAndActualResults($);
83 sub dumpToolDidCrash();
84 sub epiloguesAndPrologues($$);
85 sub expectedDirectoryForTest($;$;$);
86 sub fileNameWithNumber($$);
87 sub htmlForResultsSection(\@$&);
88 sub isTextOnlyTest($);
89 sub launchWithCurrentEnv(@);
90 sub numericcmp($$);
91 sub openDiffTool();
92 sub openDumpTool();
93 sub openHTTPDIfNeeded();
94 sub parseLeaksandPrintUniqueLeaks();
95 sub openWebSocketServerIfNeeded();
96 sub pathcmp($$);
97 sub printFailureMessageForTest($$);
98 sub processIgnoreTests($$);
99 sub readFromDumpToolWithTimer(**);
100 sub readSkippedFiles($);
101 sub recordActualResultsAndDiff($$);
102 sub sampleDumpTool();
103 sub setFileHandleNonBlocking(*$);
104 sub slowestcmp($$);
105 sub splitpath($);
106 sub stripExtension($);
107 sub stripMetrics($$);
108 sub testCrashedOrTimedOut($$$$$);
109 sub toURL($);
110 sub toWindowsPath($);
111 sub validateSkippedArg($$;$);
112 sub writeToFile($$);
113
114 # Argument handling
115 my $addPlatformExceptions = 0;
116 my $complexText = 0;
117 my $exitAfterNFailures = 0;
118 my $generateNewResults = isAppleMacWebKit() ? 1 : 0;
119 my $guardMalloc = '';
120 my $httpdPort = 8000;
121 my $httpdSSLPort = 8443;
122 my $ignoreMetrics = 0;
123 my $webSocketPort = 8880;
124 # wss is disabled until all platforms support pyOpenSSL.
125 # my $webSocketSecurePort = 9323;
126 my $ignoreTests = '';
127 my $iterations = 1;
128 my $launchSafari = 1;
129 my $mergeDepth;
130 my $pixelTests = '';
131 my $platform;
132 my $quiet = '';
133 my $randomizeTests = 0;
134 my $repeatEach = 1;
135 my $report10Slowest = 0;
136 my $resetResults = 0;
137 my $reverseTests = 0;
138 my $root;
139 my $runSample = 1;
140 my $shouldCheckLeaks = 0;
141 my $showHelp = 0;
142 my $stripEditingCallbacks = isCygwin();
143 my $testHTTP = 1;
144 my $testMedia = 1;
145 my $testResultsDirectory = "/tmp/layout-test-results";
146 my $testsPerDumpTool = 1000;
147 my $threaded = 0;
148 # DumpRenderTree has an internal timeout of 15 seconds, so this must be > 15.
149 my $timeoutSeconds = 20;
150 my $tolerance = 0;
151 my $treatSkipped = "default";
152 my $useRemoteLinksToTests = 0;
153 my $useValgrind = 0;
154 my $verbose = 0;
155
156 my @leaksFilenames;
157
158 # Default to --no-http for wx for now.
159 $testHTTP = 0 if (isWx());
160
161 my $expectedTag = "expected";
162 my $actualTag = "actual";
163 my $prettyDiffTag = "pretty-diff";
164 my $diffsTag = "diffs";
165 my $errorTag = "stderr";
166
167 my @macPlatforms = ("mac-tiger", "mac-leopard", "mac-snowleopard", "mac");
168
169 if (isAppleMacWebKit()) {
170     if (isTiger()) {
171         $platform = "mac-tiger";
172         $tolerance = 1.0;
173     } elsif (isLeopard()) {
174         $platform = "mac-leopard";
175         $tolerance = 0.1;
176     } elsif (isSnowLeopard()) {
177         $platform = "mac-snowleopard";
178         $tolerance = 0.1;
179     } else {
180         $platform = "mac";
181     }
182 } elsif (isQt()) {
183     if (isDarwin()) {
184         $platform = "qt-mac";
185     } elsif (isLinux()) {
186         $platform = "qt-linux";
187     } elsif (isWindows() || isCygwin()) {
188         $platform = "qt-win";
189     } else {
190         $platform = "qt";
191     }
192 } elsif (isGtk()) {
193     $platform = "gtk";
194     if (!$ENV{"WEBKIT_TESTFONTS"}) {
195         print "The WEBKIT_TESTFONTS environment variable is not defined.\n";
196         print "You must set it before running the tests.\n";
197         print "Use git to grab the actual fonts from http://gitorious.org/qtwebkit/testfonts\n";
198         exit 1;
199     }
200 } elsif (isWx()) {
201     $platform = "wx";
202 } elsif (isCygwin()) {
203     $platform = "win";
204 }
205
206 if (!defined($platform)) {
207     print "WARNING: Your platform is not recognized. Any platform-specific results will be generated in platform/undefined.\n";
208     $platform = "undefined";
209 }
210
211 my $programName = basename($0);
212 my $launchSafariDefault = $launchSafari ? "launch" : "do not launch";
213 my $httpDefault = $testHTTP ? "run" : "do not run";
214 my $sampleDefault = $runSample ? "run" : "do not run";
215
216 my $usage = <<EOF;
217 Usage: $programName [options] [testdir|testpath ...]
218   --add-platform-exceptions       Put new results for non-platform-specific failing tests into the platform-specific results directory
219   --complex-text                  Use the complex text code path for all text (Mac OS X and Windows only)
220   -c|--configuration config       Set DumpRenderTree build configuration
221   -g|--guard-malloc               Enable malloc guard
222   --exit-after-n-failures N       Exit after the first N failures instead of running all tests
223   -h|--help                       Show this help message
224   --[no-]http                     Run (or do not run) http tests (default: $httpDefault)
225   -i|--ignore-tests               Comma-separated list of directories or tests to ignore
226   --iterations n                  Number of times to run the set of tests (e.g. ABCABCABC)
227   --[no-]launch-safari            Launch (or do not launch) Safari to display test results (default: $launchSafariDefault)
228   -l|--leaks                      Enable leaks checking
229   --[no-]new-test-results         Generate results for new tests
230   --nthly n                       Restart DumpRenderTree every n tests (default: $testsPerDumpTool)
231   -p|--pixel-tests                Enable pixel tests
232   --tolerance t                   Ignore image differences less than this percentage (default: $tolerance)
233   --platform                      Override the detected platform to use for tests and results (default: $platform)
234   --port                          Web server port to use with http tests
235   -q|--quiet                      Less verbose output
236   --reset-results                 Reset ALL results (including pixel tests if --pixel-tests is set)
237   -o|--results-directory          Output results directory (default: $testResultsDirectory)
238   --random                        Run the tests in a random order
239   --repeat-each n                 Number of times to run each test (e.g. AAABBBCCC)
240   --reverse                       Run the tests in reverse alphabetical order
241   --root                          Path to root tools build
242   --[no-]sample-on-timeout        Run sample on timeout (default: $sampleDefault) (Mac OS X only)
243   -1|--singly                     Isolate each test case run (implies --nthly 1 --verbose)
244   --skipped=[default|ignore|only] Specifies how to treat the Skipped file
245                                      default: Tests/directories listed in the Skipped file are not tested
246                                      ignore:  The Skipped file is ignored
247                                      only:    Only those tests/directories listed in the Skipped file will be run
248   --slowest                       Report the 10 slowest tests
249   --ignore-metrics                Ignore metrics in tests
250   --[no-]strip-editing-callbacks  Remove editing callbacks from expected results
251   -t|--threaded                   Run a concurrent JavaScript thead with each test
252   --timeout t                     Sets the number of seconds before a test times out (default: $timeoutSeconds)
253   --valgrind                      Run DumpRenderTree inside valgrind (Qt/Linux only)
254   -v|--verbose                    More verbose output (overrides --quiet)
255   -m|--merge-leak-depth arg       Merges leak callStacks and prints the number of unique leaks beneath a callstack depth of arg.  Defaults to 5.
256   --use-remote-links-to-tests     Link to test files within the SVN repository in the results.
257 EOF
258
259 setConfiguration();
260
261 my $getOptionsResult = GetOptions(
262     'add-platform-exceptions' => \$addPlatformExceptions,
263     'complex-text' => \$complexText,
264     'exit-after-n-failures=i' => \$exitAfterNFailures,
265     'guard-malloc|g' => \$guardMalloc,
266     'help|h' => \$showHelp,
267     'http!' => \$testHTTP,
268     'ignore-metrics!' => \$ignoreMetrics,
269     'ignore-tests|i=s' => \$ignoreTests,
270     'iterations=i' => \$iterations,
271     'launch-safari!' => \$launchSafari,
272     'leaks|l' => \$shouldCheckLeaks,
273     'merge-leak-depth|m:5' => \$mergeDepth,
274     'new-test-results!' => \$generateNewResults,
275     'nthly=i' => \$testsPerDumpTool,
276     'pixel-tests|p' => \$pixelTests,
277     'platform=s' => \$platform,
278     'port=i' => \$httpdPort,
279     'quiet|q' => \$quiet,
280     'random' => \$randomizeTests,
281     'repeat-each=i' => \$repeatEach,
282     'reset-results' => \$resetResults,
283     'results-directory|o=s' => \$testResultsDirectory,
284     'reverse' => \$reverseTests,
285     'root=s' => \$root,
286     'sample-on-timeout!' => \$runSample,
287     'singly|1' => sub { $testsPerDumpTool = 1; },
288     'skipped=s' => \&validateSkippedArg,
289     'slowest' => \$report10Slowest,
290     'strip-editing-callbacks!' => \$stripEditingCallbacks,
291     'threaded|t' => \$threaded,
292     'timeout=i' => \$timeoutSeconds,
293     'tolerance=f' => \$tolerance,
294     'use-remote-links-to-tests' => \$useRemoteLinksToTests,
295     'valgrind' => \$useValgrind,
296     'verbose|v' => \$verbose,
297 );
298
299 if (!$getOptionsResult || $showHelp) {
300     print STDERR $usage;
301     exit 1;
302 }
303
304 my $ignoreSkipped = $treatSkipped eq "ignore";
305 my $skippedOnly = $treatSkipped eq "only";
306
307 my $configuration = configuration();
308
309 $verbose = 1 if $testsPerDumpTool == 1;
310
311 if ($shouldCheckLeaks && $testsPerDumpTool > 1000) {
312     print STDERR "\nWARNING: Running more than 1000 tests at a time with MallocStackLogging enabled may cause a crash.\n\n";
313 }
314
315 # Stack logging does not play well with QuickTime on Tiger (rdar://problem/5537157)
316 $testMedia = 0 if $shouldCheckLeaks && isTiger();
317
318 # Generating remote links causes a lot of unnecessary spew on GTK build bot
319 $useRemoteLinksToTests = 0 if isGtk();
320
321 setConfigurationProductDir(Cwd::abs_path($root)) if (defined($root));
322 my $productDir = productDir();
323 $productDir .= "/bin" if isQt();
324 $productDir .= "/Programs" if isGtk();
325
326 chdirWebKit();
327
328 if (!defined($root)) {
329     print STDERR "Running build-dumprendertree\n";
330
331     local *DEVNULL;
332     my ($childIn, $childOut, $childErr);
333     if ($quiet) {
334         open(DEVNULL, ">", File::Spec->devnull()) or die "Failed to open /dev/null";
335         $childOut = ">&DEVNULL";
336         $childErr = ">&DEVNULL";
337     } else {
338         # When not quiet, let the child use our stdout/stderr.
339         $childOut = ">&STDOUT";
340         $childErr = ">&STDERR";
341     }
342
343     my @args = argumentsForConfiguration();
344     my $buildProcess = open3($childIn, $childOut, $childErr, "WebKitTools/Scripts/build-dumprendertree", @args) or die "Failed to run build-dumprendertree";
345     close($childIn);
346     waitpid $buildProcess, 0;
347     my $buildResult = $?;
348     close($childOut);
349     close($childErr);
350
351     close DEVNULL if ($quiet);
352
353     if ($buildResult) {
354         print STDERR "Compiling DumpRenderTree failed!\n";
355         exit exitStatus($buildResult);
356     }
357 }
358
359 my $dumpToolName = "DumpRenderTree";
360 $dumpToolName .= "_debug" if isCygwin() && configurationForVisualStudio() !~ /^Release|Debug_Internal$/;
361 my $dumpTool = "$productDir/$dumpToolName";
362 die "can't find executable $dumpToolName (looked in $productDir)\n" unless -x $dumpTool;
363
364 my $imageDiffTool = "$productDir/ImageDiff";
365 $imageDiffTool .= "_debug" if isCygwin() && configurationForVisualStudio() !~ /^Release|Debug_Internal$/;
366 die "can't find executable $imageDiffTool (looked in $productDir)\n" if $pixelTests && !-x $imageDiffTool;
367
368 checkFrameworks() unless isCygwin();
369
370 if (isAppleMacWebKit()) {
371     push @INC, $productDir;
372     require DumpRenderTreeSupport;
373 }
374
375 my $layoutTestsName = "LayoutTests";
376 my $testDirectory = File::Spec->rel2abs($layoutTestsName);
377 my $expectedDirectory = $testDirectory;
378 my $platformBaseDirectory = catdir($testDirectory, "platform");
379 my $platformTestDirectory = catdir($platformBaseDirectory, $platform);
380 my @platformResultHierarchy = buildPlatformResultHierarchy();
381 my @platformTestHierarchy = buildPlatformTestHierarchy(@platformResultHierarchy);
382
383 $expectedDirectory = $ENV{"WebKitExpectedTestResultsDirectory"} if $ENV{"WebKitExpectedTestResultsDirectory"};
384
385 my $testResults = catfile($testResultsDirectory, "results.html");
386
387 print "Running tests from $testDirectory\n";
388 if ($pixelTests) {
389     print "Enabling pixel tests with a tolerance of $tolerance%\n";
390     if (isDarwin()) {
391         print "WARNING: Temporarily changing the main display color profile:\n";
392         print "\tThe colors on your screen will change for the duration of the testing.\n";
393         print "\tThis allows the pixel tests to have consistent color values across all machines.\n";
394         
395         if (isPerianInstalled()) {
396             print "WARNING: Perian's QuickTime component is installed and this may affect pixel test results!\n";
397             print "\tYou should avoid generating new pixel results in this environment.\n";
398             print "\tSee https://bugs.webkit.org/show_bug.cgi?id=22615 for details.\n";
399         }
400     }
401 }
402
403 system "ln", "-s", $testDirectory, "/tmp/LayoutTests" unless -x "/tmp/LayoutTests";
404
405 my %ignoredFiles = ( "results.html" => 1 );
406 my %ignoredDirectories = map { $_ => 1 } qw(platform);
407 my %ignoredLocalDirectories = map { $_ => 1 } qw(.svn _svn resources script-tests);
408 my %supportedFileExtensions = map { $_ => 1 } qw(html shtml xml xhtml pl php);
409
410 if (!checkWebCoreMathMLSupport(0)) { 
411     $ignoredDirectories{'mathml'} = 1;
412 }
413
414 # FIXME: We should fix webkitdirs.pm:hasSVG/WMLSupport() to do the correct feature detection for Cygwin.
415 if (checkWebCoreSVGSupport(0)) { 
416     $supportedFileExtensions{'svg'} = 1;
417 } elsif (isCygwin()) {
418     $supportedFileExtensions{'svg'} = 1;
419 } else {
420     $ignoredLocalDirectories{'svg'} = 1;
421 }
422
423 if (!$testHTTP) {
424     $ignoredDirectories{'http'} = 1;
425     $ignoredDirectories{'websocket'} = 1;
426 }
427
428 if (!$testMedia) {
429     $ignoredDirectories{'media'} = 1;
430     $ignoredDirectories{'http/tests/media'} = 1;
431 }
432
433 if (!checkWebCoreAcceleratedCompositingSupport(0)) {
434     $ignoredDirectories{'compositing'} = 1;
435 }
436
437 if (!checkWebCore3DRenderingSupport(0)) {
438     $ignoredDirectories{'animations/3d'} = 1;
439     $ignoredDirectories{'transforms/3d'} = 1;
440 }
441
442 if (!checkWebCore3DCanvasSupport(0)) {
443     $ignoredDirectories{'fast/canvas/webgl'} = 1;
444 }
445
446 if (checkWebCoreWMLSupport(0)) { 
447     $supportedFileExtensions{'wml'} = 1;
448 } else {
449     $ignoredDirectories{'http/tests/wml'} = 1;
450     $ignoredDirectories{'fast/wml'} = 1;
451     $ignoredDirectories{'wml'} = 1;
452 }
453
454 if (!checkWebCoreXHTMLMPSupport(0)) {
455     $ignoredDirectories{'fast/xhtmlmp'} = 1;
456 }
457
458 if (!checkWebCoreWCSSSupport(0)) {
459     $ignoredDirectories{'fast/wcss'} = 1;
460 }
461
462 processIgnoreTests($ignoreTests, "ignore-tests") if $ignoreTests;
463 if (!$ignoreSkipped) {
464     if (!$skippedOnly || @ARGV == 0) {
465         readSkippedFiles("");
466     } else {
467         # Since readSkippedFiles() appends to @ARGV, we must use a foreach
468         # loop so that we only iterate over the original argument list.
469         foreach my $argnum (0 .. $#ARGV) {
470             readSkippedFiles(shift @ARGV);
471         }
472     }
473 }
474
475 my @tests = findTestsToRun();
476
477 die "no tests to run\n" if !@tests;
478
479 my %counts;
480 my %tests;
481 my %imagesPresent;
482 my %imageDifferences;
483 my %durations;
484 my $count = 0;
485 my $leaksOutputFileNumber = 1;
486 my $totalLeaks = 0;
487
488 my @toolArgs = ();
489 push @toolArgs, "--pixel-tests" if $pixelTests;
490 push @toolArgs, "--threaded" if $threaded;
491 push @toolArgs, "--complex-text" if $complexText;
492 push @toolArgs, "-";
493
494 my @diffToolArgs = ();
495 push @diffToolArgs, "--tolerance", $tolerance;
496
497 $| = 1;
498
499 my $dumpToolPID;
500 my $isDumpToolOpen = 0;
501 my $dumpToolCrashed = 0;
502 my $imageDiffToolPID;
503 my $isDiffToolOpen = 0;
504
505 my $atLineStart = 1;
506 my $lastDirectory = "";
507
508 my $isHttpdOpen = 0;
509 my $isWebSocketServerOpen = 0;
510 my $webSocketServerPID = 0;
511 my $failedToStartWebSocketServer = 0;
512 # wss is disabled until all platforms support pyOpenSSL.
513 # my $webSocketSecureServerPID = 0;
514
515 sub catch_pipe { $dumpToolCrashed = 1; }
516 $SIG{"PIPE"} = "catch_pipe";
517
518 print "Testing ", scalar @tests, " test cases";
519 print " $iterations times" if ($iterations > 1);
520 print ", repeating each test $repeatEach times" if ($repeatEach > 1);
521 print ".\n";
522
523 my $overallStartTime = time;
524
525 my %expectedResultPaths;
526
527 my @originalTests = @tests;
528 # Add individual test repetitions
529 if ($repeatEach > 1) {
530     @tests = ();
531     foreach my $test (@originalTests) {
532         for (my $i = 0; $i < $repeatEach; $i++) {
533             push(@tests, $test);
534         }
535     }
536 }
537 # Add test set repetitions
538 for (my $i = 1; $i < $iterations; $i++) {
539     push(@tests, @originalTests);
540 }
541
542 for my $test (@tests) {
543     my $newDumpTool = not $isDumpToolOpen;
544     openDumpTool();
545
546     my $base = stripExtension($test);
547     my $expectedExtension = ".txt";
548     
549     my $dir = $base;
550     $dir =~ s|/[^/]+$||;
551
552     if ($newDumpTool || $dir ne $lastDirectory) {
553         foreach my $logue (epiloguesAndPrologues($newDumpTool ? "" : $lastDirectory, $dir)) {
554             if (isCygwin()) {
555                 $logue = toWindowsPath($logue);
556             } else {
557                 $logue = canonpath($logue);
558             }
559             if ($verbose) {
560                 print "running epilogue or prologue $logue\n";
561             }
562             print OUT "$logue\n";
563             # Throw away output from DumpRenderTree.
564             # Once for the test output and once for pixel results (empty)
565             while (<IN>) {
566                 last if /#EOF/;
567             }
568             while (<IN>) {
569                 last if /#EOF/;
570             }
571         }
572     }
573
574     if ($verbose) {
575         print "running $test -> ";
576         $atLineStart = 0;
577     } elsif (!$quiet) {
578         if ($dir ne $lastDirectory) {
579             print "\n" unless $atLineStart;
580             print "$dir ";
581         }
582         print ".";
583         $atLineStart = 0;
584     }
585
586     $lastDirectory = $dir;
587
588     my $result;
589
590     my $startTime = time if $report10Slowest;
591
592     # Try to read expected hash file for pixel tests
593     my $suffixExpectedHash = "";
594     if ($pixelTests && !$resetResults) {
595         my $expectedPixelDir = expectedDirectoryForTest($base, 0, "png");
596         if (open EXPECTEDHASH, "$expectedPixelDir/$base-$expectedTag.checksum") {
597             my $expectedHash = <EXPECTEDHASH>;
598             chomp($expectedHash);
599             close EXPECTEDHASH;
600             
601             # Format expected hash into a suffix string that is appended to the path / URL passed to DRT
602             $suffixExpectedHash = "'$expectedHash";
603         }
604     }
605
606     if ($test =~ /^http\//) {
607         openHTTPDIfNeeded();
608         if ($test !~ /^http\/tests\/local\// && $test !~ /^http\/tests\/ssl\// && $test !~ /^http\/tests\/wml\// && $test !~ /^http\/tests\/media\//) {
609             my $path = canonpath($test);
610             $path =~ s/^http\/tests\///;
611             print OUT "http://127.0.0.1:$httpdPort/$path$suffixExpectedHash\n";
612         } elsif ($test =~ /^http\/tests\/ssl\//) {
613             my $path = canonpath($test);
614             $path =~ s/^http\/tests\///;
615             print OUT "https://127.0.0.1:$httpdSSLPort/$path$suffixExpectedHash\n";
616         } else {
617             my $testPath = "$testDirectory/$test";
618             if (isCygwin()) {
619                 $testPath = toWindowsPath($testPath);
620             } else {
621                 $testPath = canonpath($testPath);
622             }
623             print OUT "$testPath$suffixExpectedHash\n";
624         }
625     } elsif ($test =~ /^websocket\//) {
626         if ($test =~ /^websocket\/tests\/local\//) {
627             my $testPath = "$testDirectory/$test";
628             if (isCygwin()) {
629                 $testPath = toWindowsPath($testPath);
630             } else {
631                 $testPath = canonpath($testPath);
632             }
633             print OUT "$testPath\n";
634         } else {
635             if (openWebSocketServerIfNeeded()) {
636                 my $path = canonpath($test);
637                 if ($test =~ /^websocket\/tests\/ssl\//) {
638                     # wss is disabled until all platforms support pyOpenSSL.
639                     print STDERR "Error: wss is disabled until all platforms support pyOpenSSL.";
640                     # print OUT "https://127.0.0.1:$webSocketSecurePort/$path\n";
641                 } else {
642                     print OUT "http://127.0.0.1:$webSocketPort/$path\n";
643                 }
644             } else {
645                 # We failed to launch the WebSocket server.  Display a useful error message rather than attempting
646                 # to run tests that expect the server to be available.
647                 my $errorMessagePath = "$testDirectory/websocket/resources/server-failed-to-start.html";
648                 $errorMessagePath = isCygwin() ? toWindowsPath($errorMessagePath) : canonpath($errorMessagePath);
649                 print OUT "$errorMessagePath\n";
650             }
651         }
652     } else {
653         my $testPath = "$testDirectory/$test";
654         if (isCygwin()) {
655             $testPath = toWindowsPath($testPath);
656         } else {
657             $testPath = canonpath($testPath);
658         }
659         print OUT "$testPath$suffixExpectedHash\n";
660     }
661
662     # DumpRenderTree is expected to dump two "blocks" to stdout for each test.
663     # Each block is terminated by a #EOF on a line by itself.
664     # The first block is the output of the test (in text, RenderTree or other formats).
665     # The second block is for optional pixel data in PNG format, and may be empty if
666     # pixel tests are not being run, or the test does not dump pixels (e.g. text tests).
667     my $readResults = readFromDumpToolWithTimer(IN, ERROR);
668
669     my $actual = $readResults->{output};
670     my $error = $readResults->{error};
671
672     $expectedExtension = $readResults->{extension};
673     my $expectedFileName = "$base-$expectedTag.$expectedExtension";
674
675     my $isText = isTextOnlyTest($actual);
676
677     my $expectedDir = expectedDirectoryForTest($base, $isText, $expectedExtension);
678     $expectedResultPaths{$base} = "$expectedDir/$expectedFileName";
679
680     unless ($readResults->{status} eq "success") {
681         my $crashed = $readResults->{status} eq "crashed";
682         testCrashedOrTimedOut($test, $base, $crashed, $actual, $error);
683         countFinishedTest($test, $base, $crashed ? "crash" : "timedout", 0);
684         next;
685     }
686
687     $durations{$test} = time - $startTime if $report10Slowest;
688
689     my $expected;
690
691     if (!$resetResults && open EXPECTED, "<", "$expectedDir/$expectedFileName") {
692         $expected = "";
693         while (<EXPECTED>) {
694             next if $stripEditingCallbacks && $_ =~ /^EDITING DELEGATE:/;
695             $expected .= $_;
696         }
697         close EXPECTED;
698     }
699
700     if ($ignoreMetrics && !$isText && defined $expected) {
701         ($actual, $expected) = stripMetrics($actual, $expected);
702     }
703
704     if ($shouldCheckLeaks && $testsPerDumpTool == 1) {
705         print "        $test -> ";
706     }
707
708     my $actualPNG = "";
709     my $diffPNG = "";
710     my $diffPercentage = "";
711     my $diffResult = "passed";
712
713     my $actualHash = "";
714     my $expectedHash = "";
715     my $actualPNGSize = 0;
716
717     while (<IN>) {
718         last if /#EOF/;
719         if (/ActualHash: ([a-f0-9]{32})/) {
720             $actualHash = $1;
721         } elsif (/ExpectedHash: ([a-f0-9]{32})/) {
722             $expectedHash = $1;
723         } elsif (/Content-Length: (\d+)\s*/) {
724             $actualPNGSize = $1;
725             read(IN, $actualPNG, $actualPNGSize);
726         }
727     }
728
729     if ($verbose && $pixelTests && !$resetResults && $actualPNGSize) {
730         if ($actualHash eq "" && $expectedHash eq "") {
731             printFailureMessageForTest($test, "WARNING: actual & expected pixel hashes are missing!");
732         } elsif ($actualHash eq "") {
733             printFailureMessageForTest($test, "WARNING: actual pixel hash is missing!");
734         } elsif ($expectedHash eq "") {
735             printFailureMessageForTest($test, "WARNING: expected pixel hash is missing!");
736         }
737     }
738
739     if ($actualPNGSize > 0) {
740         my $expectedPixelDir = expectedDirectoryForTest($base, 0, "png");
741
742         if (!$resetResults && ($expectedHash ne $actualHash || ($actualHash eq "" && $expectedHash eq ""))) {
743             if (-f "$expectedPixelDir/$base-$expectedTag.png") {
744                 my $expectedPNGSize = -s "$expectedPixelDir/$base-$expectedTag.png";
745                 my $expectedPNG = "";
746                 open EXPECTEDPNG, "$expectedPixelDir/$base-$expectedTag.png";
747                 read(EXPECTEDPNG, $expectedPNG, $expectedPNGSize);
748
749                 openDiffTool();
750                 print DIFFOUT "Content-Length: $actualPNGSize\n";
751                 print DIFFOUT $actualPNG;
752
753                 print DIFFOUT "Content-Length: $expectedPNGSize\n";
754                 print DIFFOUT $expectedPNG;
755
756                 while (<DIFFIN>) {
757                     last if /^error/ || /^diff:/;
758                     if (/Content-Length: (\d+)\s*/) {
759                         read(DIFFIN, $diffPNG, $1);
760                     }
761                 }
762
763                 if (/^diff: (.+)% (passed|failed)/) {
764                     $diffPercentage = $1;
765                     $imageDifferences{$base} = $diffPercentage;
766                     $diffResult = $2;
767                 }
768                 
769                 if ($diffPercentage == 0) {
770                     printFailureMessageForTest($test, "pixel hash failed (but pixel test still passes)");
771                 }
772             } elsif ($verbose) {
773                 printFailureMessageForTest($test, "WARNING: expected image is missing!");
774             }
775         }
776
777         if ($resetResults || !-f "$expectedPixelDir/$base-$expectedTag.png") {
778             mkpath catfile($expectedPixelDir, dirname($base)) if $testDirectory ne $expectedPixelDir;
779             writeToFile("$expectedPixelDir/$base-$expectedTag.png", $actualPNG);
780         }
781
782         if ($actualHash ne "" && ($resetResults || !-f "$expectedPixelDir/$base-$expectedTag.checksum")) {
783             writeToFile("$expectedPixelDir/$base-$expectedTag.checksum", $actualHash);
784         }
785     }
786
787     if (dumpToolDidCrash()) {
788         $result = "crash";
789         testCrashedOrTimedOut($test, $base, 1, $actual, $error);
790     } elsif (!defined $expected) {
791         if ($verbose) {
792             print "new " . ($resetResults ? "result" : "test") ."\n";
793             $atLineStart = 1;
794         }
795         $result = "new";
796
797         if ($generateNewResults || $resetResults) {
798             mkpath catfile($expectedDir, dirname($base)) if $testDirectory ne $expectedDir;
799             writeToFile("$expectedDir/$expectedFileName", $actual);
800         }
801         deleteExpectedAndActualResults($base);
802         recordActualResultsAndDiff($base, $actual);
803         if (!$resetResults) {
804             # Always print the file name for new tests, as they will probably need some manual inspection.
805             # in verbose mode we already printed the test case, so no need to do it again.
806             unless ($verbose) {
807                 print "\n" unless $atLineStart;
808                 print "$test -> ";
809             }
810             my $resultsDir = catdir($expectedDir, dirname($base));
811             if ($generateNewResults) {
812                 print "new (results generated in $resultsDir)\n";
813             } else {
814                 print "new\n";
815             }
816             $atLineStart = 1;
817         }
818     } elsif ($actual eq $expected && $diffResult eq "passed") {
819         if ($verbose) {
820             print "succeeded\n";
821             $atLineStart = 1;
822         }
823         $result = "match";
824         deleteExpectedAndActualResults($base);
825     } else {
826         $result = "mismatch";
827
828         my $pixelTestFailed = $pixelTests && $diffPNG && $diffPNG ne "";
829         my $testFailed = $actual ne $expected;
830
831         my $message = !$testFailed ? "pixel test failed" : "failed";
832
833         if (($testFailed || $pixelTestFailed) && $addPlatformExceptions) {
834             my $testBase = catfile($testDirectory, $base);
835             my $expectedBase = catfile($expectedDir, $base);
836             my $testIsMaximallyPlatformSpecific = $testBase =~ m|^\Q$platformTestDirectory\E/|;
837             my $expectedResultIsMaximallyPlatformSpecific = $expectedBase =~ m|^\Q$platformTestDirectory\E/|;
838             if (!$testIsMaximallyPlatformSpecific && !$expectedResultIsMaximallyPlatformSpecific) {
839                 mkpath catfile($platformTestDirectory, dirname($base));
840                 if ($testFailed) {
841                     my $expectedFile = catfile($platformTestDirectory, "$expectedFileName");
842                     writeToFile("$expectedFile", $actual);
843                 }
844                 if ($pixelTestFailed) {
845                     my $expectedFile = catfile($platformTestDirectory, "$base-$expectedTag.checksum");
846                     writeToFile("$expectedFile", $actualHash);
847
848                     $expectedFile = catfile($platformTestDirectory, "$base-$expectedTag.png");
849                     writeToFile("$expectedFile", $actualPNG);
850                 }
851                 $message .= " (results generated in $platformTestDirectory)";
852             }
853         }
854
855         printFailureMessageForTest($test, $message);
856
857         my $dir = "$testResultsDirectory/$base";
858         $dir =~ s|/([^/]+)$|| or die "Failed to find test name from base\n";
859         my $testName = $1;
860         mkpath $dir;
861
862         deleteExpectedAndActualResults($base);
863         recordActualResultsAndDiff($base, $actual);
864
865         if ($pixelTestFailed) {
866             $imagesPresent{$base} = 1;
867
868             writeToFile("$testResultsDirectory/$base-$actualTag.png", $actualPNG);
869             writeToFile("$testResultsDirectory/$base-$diffsTag.png", $diffPNG);
870
871             my $expectedPixelDir = expectedDirectoryForTest($base, 0, "png");
872             copy("$expectedPixelDir/$base-$expectedTag.png", "$testResultsDirectory/$base-$expectedTag.png");
873
874             open DIFFHTML, ">$testResultsDirectory/$base-$diffsTag.html" or die;
875             print DIFFHTML "<html>\n";
876             print DIFFHTML "<head>\n";
877             print DIFFHTML "<title>$base Image Compare</title>\n";
878             print DIFFHTML "<script language=\"Javascript\" type=\"text/javascript\">\n";
879             print DIFFHTML "var currentImage = 0;\n";
880             print DIFFHTML "var imageNames = new Array(\"Actual\", \"Expected\");\n";
881             print DIFFHTML "var imagePaths = new Array(\"$testName-$actualTag.png\", \"$testName-$expectedTag.png\");\n";
882             if (-f "$testDirectory/$base-w3c.png") {
883                 copy("$testDirectory/$base-w3c.png", "$testResultsDirectory/$base-w3c.png");
884                 print DIFFHTML "imageNames.push(\"W3C\");\n";
885                 print DIFFHTML "imagePaths.push(\"$testName-w3c.png\");\n";
886             }
887             print DIFFHTML "function animateImage() {\n";
888             print DIFFHTML "    var image = document.getElementById(\"animatedImage\");\n";
889             print DIFFHTML "    var imageText = document.getElementById(\"imageText\");\n";
890             print DIFFHTML "    image.src = imagePaths[currentImage];\n";
891             print DIFFHTML "    imageText.innerHTML = imageNames[currentImage] + \" Image\";\n";
892             print DIFFHTML "    currentImage = (currentImage + 1) % imageNames.length;\n";
893             print DIFFHTML "    setTimeout('animateImage()',2000);\n";
894             print DIFFHTML "}\n";
895             print DIFFHTML "</script>\n";
896             print DIFFHTML "</head>\n";
897             print DIFFHTML "<body onLoad=\"animateImage();\">\n";
898             print DIFFHTML "<table>\n";
899             if ($diffPercentage) {
900                 print DIFFHTML "<tr>\n";
901                 print DIFFHTML "<td>Difference between images: <a href=\"$testName-$diffsTag.png\">$diffPercentage%</a></td>\n";
902                 print DIFFHTML "</tr>\n";
903             }
904             print DIFFHTML "<tr>\n";
905             print DIFFHTML "<td><a href=\"" . toURL("$testDirectory/$test") . "\">test file</a></td>\n";
906             print DIFFHTML "</tr>\n";
907             print DIFFHTML "<tr>\n";
908             print DIFFHTML "<td id=\"imageText\" style=\"text-weight: bold;\">Actual Image</td>\n";
909             print DIFFHTML "</tr>\n";
910             print DIFFHTML "<tr>\n";
911             print DIFFHTML "<td><img src=\"$testName-$actualTag.png\" id=\"animatedImage\"></td>\n";
912             print DIFFHTML "</tr>\n";
913             print DIFFHTML "</table>\n";
914             print DIFFHTML "</body>\n";
915             print DIFFHTML "</html>\n";
916         }
917     }
918
919     if ($error) {
920         my $dir = "$testResultsDirectory/$base";
921         $dir =~ s|/([^/]+)$|| or die "Failed to find test name from base\n";
922         mkpath $dir;
923         
924         writeToFile("$testResultsDirectory/$base-$errorTag.txt", $error);
925         
926         $counts{error}++;
927         push @{$tests{error}}, $test;
928     }
929
930     countFinishedTest($test, $base, $result, $isText);
931
932     # --reset-results does not check pass vs. fail, so exitAfterNFailures makes no sense with --reset-results.
933     if ($exitAfterNFailures && !$resetResults) {
934         my $passCount = $counts{match} || 0; # $counts{match} will be undefined if we've not yet passed a test (e.g. the first test fails).
935         my $failureCount = $count - $passCount; # "Failure" here includes new tests, timeouts, crashes, etc.
936         if ($failureCount >= $exitAfterNFailures) {
937             print "\nExiting early after $failureCount failures. $count tests run.";
938             closeDumpTool();
939             last;
940         }
941     }
942 }
943 printf "\n%0.2fs total testing time\n", (time - $overallStartTime) . "";
944
945 !$isDumpToolOpen || die "Failed to close $dumpToolName.\n";
946
947 closeHTTPD();
948 closeWebSocketServer();
949
950 # Because multiple instances of this script are running concurrently we cannot 
951 # safely delete this symlink.
952 # system "rm /tmp/LayoutTests";
953
954 # FIXME: Do we really want to check the image-comparison tool for leaks every time?
955 if ($isDiffToolOpen && $shouldCheckLeaks) {
956     $totalLeaks += countAndPrintLeaks("ImageDiff", $imageDiffToolPID, "$testResultsDirectory/ImageDiff-leaks.txt");
957 }
958
959 if ($totalLeaks) {
960     if ($mergeDepth) {
961         parseLeaksandPrintUniqueLeaks();
962     } else { 
963         print "\nWARNING: $totalLeaks total leaks found!\n";
964         print "See above for individual leaks results.\n" if ($leaksOutputFileNumber > 2);
965     }
966 }
967
968 close IN;
969 close OUT;
970 close ERROR;
971
972 if ($report10Slowest) {
973     print "\n\nThe 10 slowest tests:\n\n";
974     my $count = 0;
975     for my $test (sort slowestcmp keys %durations) {
976         printf "%0.2f secs: %s\n", $durations{$test}, $test;
977         last if ++$count == 10;
978     }
979 }
980
981 print "\n";
982
983 if ($skippedOnly && $counts{"match"}) {
984     print "The following tests are in the Skipped file (" . File::Spec->abs2rel("$platformTestDirectory/Skipped", $testDirectory) . "), but succeeded:\n";
985     foreach my $test (@{$tests{"match"}}) {
986         print "  $test\n";
987     }
988 }
989
990 if ($resetResults || ($counts{match} && $counts{match} == $count)) {
991     print "all $count test cases succeeded\n";
992     unlink $testResults;
993     exit;
994 }
995
996 printResults();
997
998 mkpath $testResultsDirectory;
999
1000 open HTML, ">", $testResults or die "Failed to open $testResults. $!";
1001 print HTML "<html>\n";
1002 print HTML "<head>\n";
1003 print HTML "<title>Layout Test Results</title>\n";
1004 print HTML "</head>\n";
1005 print HTML "<body>\n";
1006
1007 if ($ignoreMetrics) {
1008     print HTML "<h4>Tested with metrics ignored.</h4>";
1009 }
1010
1011 print HTML htmlForResultsSection(@{$tests{mismatch}}, "Tests where results did not match expected results", \&linksForMismatchTest);
1012 print HTML htmlForResultsSection(@{$tests{timedout}}, "Tests that timed out", \&linksForErrorTest);
1013 print HTML htmlForResultsSection(@{$tests{crash}}, "Tests that caused the DumpRenderTree tool to crash", \&linksForErrorTest);
1014 print HTML htmlForResultsSection(@{$tests{error}}, "Tests that had stderr output", \&linksForErrorTest);
1015 print HTML htmlForResultsSection(@{$tests{new}}, "Tests that had no expected results (probably new)", \&linksForNewTest);
1016
1017 print HTML "</body>\n";
1018 print HTML "</html>\n";
1019 close HTML;
1020
1021 my @configurationArgs = argumentsForConfiguration();
1022
1023 if (isGtk()) {
1024   system "WebKitTools/Scripts/run-launcher", @configurationArgs, "file://".$testResults if $launchSafari;
1025 } elsif (isQt()) {
1026   unshift @configurationArgs, qw(-graphicssystem raster -style windows);
1027   system "WebKitTools/Scripts/run-launcher", @configurationArgs, "file://".$testResults if $launchSafari;
1028 } elsif (isCygwin()) {
1029   system "cygstart", $testResults if $launchSafari;
1030 } else {
1031   system "WebKitTools/Scripts/run-safari", @configurationArgs, "-NSOpen", $testResults if $launchSafari;
1032 }
1033
1034 closeCygpaths() if isCygwin();
1035
1036 exit 1;
1037
1038 sub countAndPrintLeaks($$$)
1039 {
1040     my ($dumpToolName, $dumpToolPID, $leaksFilePath) = @_;
1041
1042     print "\n" unless $atLineStart;
1043     $atLineStart = 1;
1044
1045     # We are excluding the following reported leaks so they don't get in our way when looking for WebKit leaks:
1046     # This allows us ignore known leaks and only be alerted when new leaks occur. Some leaks are in the old
1047     # versions of the system frameworks that are being used by the leaks bots. Even though a leak has been
1048     # fixed, it will be listed here until the bot has been updated with the newer frameworks.
1049
1050     my @typesToExclude = (
1051     );
1052
1053     my @callStacksToExclude = (
1054         "Flash_EnforceLocalSecurity" # leaks in Flash plug-in code, rdar://problem/4449747
1055     );
1056
1057     if (isTiger()) {
1058         # Leak list for the version of Tiger used on the build bot.
1059         push @callStacksToExclude, (
1060             "CFRunLoopRunSpecific \\| malloc_zone_malloc", "CFRunLoopRunSpecific \\| CFAllocatorAllocate ", # leak in CFRunLoopRunSpecific, rdar://problem/4670839
1061             "CGImageSourceGetPropertiesAtIndex", # leak in ImageIO, rdar://problem/4628809
1062             "FOGetCoveredUnicodeChars", # leak in ATS, rdar://problem/3943604
1063             "GetLineDirectionPreference", "InitUnicodeUtilities", # leaks tool falsely reporting leak in CFNotificationCenterAddObserver, rdar://problem/4964790
1064             "ICCFPrefWrapper::GetPrefDictionary", # leaks in Internet Config. code, rdar://problem/4449794
1065             "NSHTTPURLProtocol setResponseHeader:", # leak in multipart/mixed-replace handling in Foundation, no Radar, but fixed in Leopard
1066             "NSURLCache cachedResponseForRequest", # leak in CFURL cache, rdar://problem/4768430
1067             "PCFragPrepareClosureFromFile", # leak in Code Fragment Manager, rdar://problem/3426998
1068             "WebCore::Selection::toRange", # bug in 'leaks', rdar://problem/4967949
1069             "WebCore::SubresourceLoader::create", # bug in 'leaks', rdar://problem/4985806
1070             "_CFPreferencesDomainDeepCopyDictionary", # leak in CFPreferences, rdar://problem/4220786
1071             "_objc_msgForward", # leak in NSSpellChecker, rdar://problem/4965278
1072             "gldGetString", # leak in OpenGL, rdar://problem/5013699
1073             "_setDefaultUserInfoFromURL", # leak in NSHTTPAuthenticator, rdar://problem/5546453 
1074             "SSLHandshake", # leak in SSL, rdar://problem/5546440 
1075             "SecCertificateCreateFromData", # leak in SSL code, rdar://problem/4464397
1076         );
1077         push @typesToExclude, (
1078             "THRD", # bug in 'leaks', rdar://problem/3387783
1079             "DRHT", # ditto (endian little hate i)
1080         );
1081     }
1082
1083     if (isLeopard()) {
1084         # Leak list for the version of Leopard used on the build bot.
1085         push @callStacksToExclude, (
1086             "CFHTTPMessageAppendBytes", # leak in CFNetwork, rdar://problem/5435912
1087             "sendDidReceiveDataCallback", # leak in CFNetwork, rdar://problem/5441619
1088             "_CFHTTPReadStreamReadMark", # leak in CFNetwork, rdar://problem/5441468
1089             "httpProtocolStart", # leak in CFNetwork, rdar://problem/5468837
1090             "_CFURLConnectionSendCallbacks", # leak in CFNetwork, rdar://problem/5441600
1091             "DispatchQTMsg", # leak in QuickTime, PPC only, rdar://problem/5667132
1092             "QTMovieContentView createVisualContext", # leak in QuickTime, PPC only, rdar://problem/5667132
1093             "_CopyArchitecturesForJVMVersion", # leak in Java, rdar://problem/5910823
1094         );
1095     }
1096
1097     if (isSnowLeopard()) {
1098         push @callStacksToExclude, (
1099             "readMakerNoteProps", # <rdar://problem/7156432> leak in ImageIO
1100             "QTKitMovieControllerView completeUISetup", # <rdar://problem/7155156> leak in QTKit
1101         );
1102     }
1103
1104     my $leaksTool = sourceDir() . "/WebKitTools/Scripts/run-leaks";
1105     my $excludeString = "--exclude-callstack '" . (join "' --exclude-callstack '", @callStacksToExclude) . "'";
1106     $excludeString .= " --exclude-type '" . (join "' --exclude-type '", @typesToExclude) . "'" if @typesToExclude;
1107
1108     print " ? checking for leaks in $dumpToolName\n";
1109     my $leaksOutput = `$leaksTool $excludeString $dumpToolPID`;
1110     my ($count, $bytes) = $leaksOutput =~ /Process $dumpToolPID: (\d+) leaks? for (\d+) total/;
1111     my ($excluded) = $leaksOutput =~ /(\d+) leaks? excluded/;
1112
1113     my $adjustedCount = $count;
1114     $adjustedCount -= $excluded if $excluded;
1115
1116     if (!$adjustedCount) {
1117         print " - no leaks found\n";
1118         unlink $leaksFilePath;
1119         return 0;
1120     } else {
1121         my $dir = $leaksFilePath;
1122         $dir =~ s|/[^/]+$|| or die;
1123         mkpath $dir;
1124
1125         if ($excluded) {
1126             print " + $adjustedCount leaks ($bytes bytes including $excluded excluded leaks) were found, details in $leaksFilePath\n";
1127         } else {
1128             print " + $count leaks ($bytes bytes) were found, details in $leaksFilePath\n";
1129         }
1130
1131         writeToFile($leaksFilePath, $leaksOutput);
1132         
1133         push @leaksFilenames, $leaksFilePath;
1134     }
1135
1136     return $adjustedCount;
1137 }
1138
1139 sub writeToFile($$)
1140 {
1141     my ($filePath, $contents) = @_;
1142     open NEWFILE, ">", "$filePath" or die "Could not create $filePath. $!\n";
1143     print NEWFILE $contents;
1144     close NEWFILE;
1145 }
1146
1147 # Break up a path into the directory (with slash) and base name.
1148 sub splitpath($)
1149 {
1150     my ($path) = @_;
1151
1152     my $pathSeparator = "/";
1153     my $dirname = dirname($path) . $pathSeparator;
1154     $dirname = "" if $dirname eq "." . $pathSeparator;
1155
1156     return ($dirname, basename($path));
1157 }
1158
1159 # Sort first by directory, then by file, so all paths in one directory are grouped
1160 # rather than being interspersed with items from subdirectories.
1161 # Use numericcmp to sort directory and filenames to make order logical.
1162 sub pathcmp($$)
1163 {
1164     my ($patha, $pathb) = @_;
1165
1166     my ($dira, $namea) = splitpath($patha);
1167     my ($dirb, $nameb) = splitpath($pathb);
1168
1169     return numericcmp($dira, $dirb) if $dira ne $dirb;
1170     return numericcmp($namea, $nameb);
1171 }
1172
1173 # Sort numeric parts of strings as numbers, other parts as strings.
1174 # Makes 1.33 come after 1.3, which is cool.
1175 sub numericcmp($$)
1176 {
1177     my ($aa, $bb) = @_;
1178
1179     my @a = split /(\d+)/, $aa;
1180     my @b = split /(\d+)/, $bb;
1181
1182     # Compare one chunk at a time.
1183     # Each chunk is either all numeric digits, or all not numeric digits.
1184     while (@a && @b) {
1185         my $a = shift @a;
1186         my $b = shift @b;
1187         
1188         # Use numeric comparison if chunks are non-equal numbers.
1189         return $a <=> $b if $a =~ /^\d/ && $b =~ /^\d/ && $a != $b;
1190
1191         # Use string comparison if chunks are any other kind of non-equal string.
1192         return $a cmp $b if $a ne $b;
1193     }
1194     
1195     # One of the two is now empty; compare lengths for result in this case.
1196     return @a <=> @b;
1197 }
1198
1199 # Sort slowest tests first.
1200 sub slowestcmp($$)
1201 {
1202     my ($testa, $testb) = @_;
1203
1204     my $dura = $durations{$testa};
1205     my $durb = $durations{$testb};
1206     return $durb <=> $dura if $dura != $durb;
1207     return pathcmp($testa, $testb);
1208 }
1209
1210 sub launchWithCurrentEnv(@)
1211 {
1212     my (@args) = @_;
1213
1214     # Dump the current environment as perl code and then put it in quotes so it is one parameter.
1215     my $environmentDumper = Data::Dumper->new([\%ENV], [qw(*ENV)]);
1216     $environmentDumper->Indent(0);
1217     $environmentDumper->Purity(1);
1218     my $allEnvVars = $environmentDumper->Dump();
1219     unshift @args, "\"$allEnvVars\"";
1220
1221     my $execScript = File::Spec->catfile(sourceDir(), qw(WebKitTools Scripts execAppWithEnv));
1222     unshift @args, $execScript;
1223     return @args;
1224 }
1225
1226 sub openDiffTool()
1227 {
1228     return if $isDiffToolOpen;
1229     return if !$pixelTests;
1230
1231     local %ENV;
1232     $ENV{MallocStackLogging} = 1 if $shouldCheckLeaks;
1233     $imageDiffToolPID = open2(\*DIFFIN, \*DIFFOUT, $imageDiffTool, launchWithCurrentEnv(@diffToolArgs)) or die "unable to open $imageDiffTool\n";
1234     $ENV{MallocStackLogging} = 0 if $shouldCheckLeaks;
1235     $isDiffToolOpen = 1;
1236 }
1237
1238 sub openDumpTool()
1239 {
1240     return if $isDumpToolOpen;
1241
1242     # Save environment variables required for the linux environment.
1243     my $homeDir = $ENV{'HOME'};
1244     my $libraryPath = $ENV{'LD_LIBRARY_PATH'};
1245     my $dyldLibraryPath = $ENV{'DYLD_LIBRARY_PATH'};
1246     my $dbusAddress = $ENV{'DBUS_SESSION_BUS_ADDRESS'};
1247     my $display = $ENV{'DISPLAY'};
1248     my $xauthority = $ENV{'XAUTHORITY'};
1249     my $testfonts = $ENV{'WEBKIT_TESTFONTS'};
1250
1251     my $homeDrive = $ENV{'HOMEDRIVE'};
1252     my $homePath = $ENV{'HOMEPATH'};
1253         
1254     local %ENV;
1255     if (isQt() || isGtk()) {
1256         if (defined $display) {
1257             $ENV{DISPLAY} = $display;
1258         } else {
1259             $ENV{DISPLAY} = ":1";
1260         }
1261         if (defined $xauthority) {
1262             $ENV{XAUTHORITY} = $xauthority;
1263         }
1264         $ENV{'WEBKIT_TESTFONTS'} = $testfonts if defined($testfonts);
1265         $ENV{HOME} = $homeDir;
1266         if (defined $libraryPath) {
1267             $ENV{LD_LIBRARY_PATH} = $libraryPath;
1268         }
1269         if (defined $dyldLibraryPath) {
1270             $ENV{DYLD_LIBRARY_PATH} = $dyldLibraryPath;
1271         }
1272         if (defined $dbusAddress) {
1273             $ENV{DBUS_SESSION_BUS_ADDRESS} = $dbusAddress;
1274         }
1275     }
1276     if (isQt()) {
1277         $ENV{QTWEBKIT_PLUGIN_PATH} = productDir() . "/lib/plugins";
1278     }
1279     $ENV{DYLD_FRAMEWORK_PATH} = $productDir;
1280     $ENV{XML_CATALOG_FILES} = ""; # work around missing /etc/catalog <rdar://problem/4292995>
1281     $ENV{DYLD_INSERT_LIBRARIES} = "/usr/lib/libgmalloc.dylib" if $guardMalloc;
1282     
1283     if (isCygwin()) {
1284         $ENV{HOMEDRIVE} = $homeDrive;
1285         $ENV{HOMEPATH} = $homePath;
1286         if ($testfonts) {
1287             $ENV{WEBKIT_TESTFONTS} = $testfonts;
1288         }
1289         setPathForRunningWebKitApp(\%ENV) if isCygwin();
1290     }
1291
1292     my @args = ($dumpTool, @toolArgs);
1293     if (isAppleMacWebKit() and !isTiger()) { 
1294         unshift @args, "arch", "-" . architecture();             
1295     }
1296
1297     if ($useValgrind) {
1298         unshift @args, "valgrind", "--suppressions=$platformBaseDirectory/qt/SuppressedValgrindErrors";
1299     } 
1300     
1301     $ENV{MallocStackLogging} = 1 if $shouldCheckLeaks;
1302     $dumpToolPID = open3(\*OUT, \*IN, \*ERROR, launchWithCurrentEnv(@args)) or die "Failed to start tool: $dumpTool\n";
1303     $ENV{MallocStackLogging} = 0 if $shouldCheckLeaks;
1304     $isDumpToolOpen = 1;
1305     $dumpToolCrashed = 0;
1306 }
1307
1308 sub closeDumpTool()
1309 {
1310     return if !$isDumpToolOpen;
1311
1312     close IN;
1313     close OUT;
1314     waitpid $dumpToolPID, 0;
1315     
1316     # check for WebCore counter leaks.
1317     if ($shouldCheckLeaks) {
1318         while (<ERROR>) {
1319             print;
1320         }
1321     }
1322     close ERROR;
1323     $isDumpToolOpen = 0;
1324 }
1325
1326 sub dumpToolDidCrash()
1327 {
1328     return 1 if $dumpToolCrashed;
1329     return 0 unless $isDumpToolOpen;
1330
1331     my $pid = waitpid(-1, WNOHANG);
1332     return 1 if ($pid == $dumpToolPID);
1333
1334     # On Mac OS X, crashing may be significantly delayed by crash reporter.
1335     return 0 unless isAppleMacWebKit();
1336
1337     return DumpRenderTreeSupport::processIsCrashing($dumpToolPID);
1338 }
1339
1340 sub openHTTPDIfNeeded()
1341 {
1342     return if $isHttpdOpen;
1343
1344     mkdir "/tmp/WebKit";
1345     
1346     if (-f "/tmp/WebKit/httpd.pid") {
1347         my $oldPid = `cat /tmp/WebKit/httpd.pid`;
1348         chomp $oldPid;
1349         if (0 != kill 0, $oldPid) {
1350             print "\nhttpd is already running: pid $oldPid, killing...\n";
1351             kill 15, $oldPid;
1352             
1353             my $retryCount = 20;
1354             while ((0 != kill 0, $oldPid) && $retryCount) {
1355                 sleep 1;
1356                 --$retryCount;
1357             }
1358             
1359             die "Timed out waiting for httpd to quit" unless $retryCount;
1360         }
1361     }
1362     
1363     my $httpdPath = "/usr/sbin/httpd";
1364     my $httpdConfig;
1365     if (isCygwin()) {
1366         my $windowsConfDirectory = "$testDirectory/http/conf/";
1367         unless (-x "/usr/lib/apache/libphp4.dll") {
1368             copy("$windowsConfDirectory/libphp4.dll", "/usr/lib/apache/libphp4.dll");
1369             chmod(0755, "/usr/lib/apache/libphp4.dll");
1370         }
1371         $httpdConfig = "$windowsConfDirectory/cygwin-httpd.conf";
1372     } elsif (isDebianBased()) {
1373         $httpdPath = "/usr/sbin/apache2";
1374         $httpdConfig = "$testDirectory/http/conf/apache2-debian-httpd.conf";
1375     } elsif (isFedoraBased()) {
1376         $httpdPath = "/usr/sbin/httpd";
1377         $httpdConfig = "$testDirectory/http/conf/fedora-httpd.conf";
1378     } else {
1379         $httpdConfig = "$testDirectory/http/conf/httpd.conf";
1380         $httpdConfig = "$testDirectory/http/conf/apache2-httpd.conf" if `$httpdPath -v` =~ m|Apache/2|;
1381     }
1382     my $documentRoot = "$testDirectory/http/tests";
1383     my $jsTestResourcesDirectory = $testDirectory . "/fast/js/resources";
1384     my $typesConfig = "$testDirectory/http/conf/mime.types";
1385     my $listen = "127.0.0.1:$httpdPort";
1386     my $absTestResultsDirectory = File::Spec->rel2abs(glob $testResultsDirectory);
1387     my $sslCertificate = "$testDirectory/http/conf/webkit-httpd.pem";
1388
1389     mkpath $absTestResultsDirectory;
1390
1391     my @args = (
1392         "-f", "$httpdConfig",
1393         "-C", "DocumentRoot \"$documentRoot\"",
1394         # Setup a link to where the js test templates are stored, use -c so that mod_alias will already be laoded.
1395         "-c", "Alias /js-test-resources \"$jsTestResourcesDirectory\"",
1396         "-C", "Listen $listen",
1397         "-c", "TypesConfig \"$typesConfig\"",
1398         "-c", "CustomLog \"$absTestResultsDirectory/access_log.txt\" common",
1399         "-c", "ErrorLog \"$absTestResultsDirectory/error_log.txt\"",
1400         # Apache wouldn't run CGIs with permissions==700 otherwise
1401         "-c", "User \"#$<\""
1402     );
1403
1404     # FIXME: Enable this on Windows once <rdar://problem/5345985> is fixed
1405     # The version of Apache we use with Cygwin does not support SSL
1406     push(@args, "-c", "SSLCertificateFile \"$sslCertificate\"") unless isCygwin();
1407
1408     open2(\*HTTPDIN, \*HTTPDOUT, $httpdPath, @args);
1409
1410     my $retryCount = 20;
1411     while (system("/usr/bin/curl -q --silent --stderr - --output " . File::Spec->devnull() . " $listen") && $retryCount) {
1412         sleep 1;
1413         --$retryCount;
1414     }
1415     
1416     die "Timed out waiting for httpd to start" unless $retryCount;
1417     
1418     $isHttpdOpen = 1;
1419 }
1420
1421 sub closeHTTPD()
1422 {
1423     return if !$isHttpdOpen;
1424
1425     close HTTPDIN;
1426     close HTTPDOUT;
1427
1428     kill 15, `cat /tmp/WebKit/httpd.pid` if -f "/tmp/WebKit/httpd.pid";
1429
1430     $isHttpdOpen = 0;
1431 }
1432
1433 sub openWebSocketServerIfNeeded()
1434 {
1435     return 1 if $isWebSocketServerOpen;
1436     return 0 if $failedToStartWebSocketServer;
1437
1438     my $webSocketServerPath = "/usr/bin/python";
1439     my $webSocketPythonPath = "WebKitTools/pywebsocket";
1440     my $webSocketHandlerDir = "$testDirectory";
1441     my $webSocketHandlerScanDir = "$testDirectory/websocket/tests";
1442     my $sslCertificate = "$testDirectory/http/conf/webkit-httpd.pem";
1443     my @args = (
1444         "WebKitTools/pywebsocket/mod_pywebsocket/standalone.py",
1445         "-p", "$webSocketPort",
1446         "-d", "$webSocketHandlerDir",
1447         "-s", "$webSocketHandlerScanDir",
1448     );
1449     # wss is disabled until all platforms support pyOpenSSL.
1450     # my @argsSecure = (
1451     #     "WebKitTools/pywebsocket/mod_pywebsocket/standalone.py",
1452     #     "-p", "$webSocketSecurePort",
1453     #     "-d", "$webSocketHandlerDir",
1454     #     "-t",
1455     #     "-k", "$sslCertificate",
1456     #     "-c", "$sslCertificate",
1457     # );
1458
1459     $ENV{"PYTHONPATH"} = $webSocketPythonPath;
1460     $webSocketServerPID = open3(\*WEBSOCKETSERVER_IN, \*WEBSOCKETSERVER_OUT, \*WEBSOCKETSERVER_ERR, $webSocketServerPath, @args);
1461     # wss is disabled until all platforms support pyOpenSSL.
1462     # $webSocketSecureServerPID = open3(\*WEBSOCKETSECURESERVER_IN, \*WEBSOCKETSECURESERVER_OUT, \*WEBSOCKETSECURESERVER_ERR, $webSocketServerPath, @argsSecure);
1463     # my @listen = ("http://127.0.0.1:$webSocketPort", "https://127.0.0.1:$webSocketSecurePort");
1464     my @listen = ("http://127.0.0.1:$webSocketPort");
1465     for (my $i = 0; $i < @listen; $i++) {
1466         my $retryCount = 10;
1467         while (system("/usr/bin/curl -k -q --silent --stderr - --output /dev/null $listen[$i]") && $retryCount) {
1468             sleep 1;
1469             --$retryCount;
1470         }
1471         unless ($retryCount) {
1472             print STDERR "Timed out waiting for WebSocketServer to start.\n";
1473             $failedToStartWebSocketServer = 1;
1474             return 0;
1475         }
1476     }
1477
1478     $isWebSocketServerOpen = 1;
1479     return 1;
1480 }
1481
1482 sub closeWebSocketServer()
1483 {
1484     return if !$isWebSocketServerOpen;
1485
1486     close WEBSOCKETSERVER_IN;
1487     close WEBSOCKETSERVER_OUT;
1488     close WEBSOCKETSERVER_ERR;
1489     kill 15, $webSocketServerPID;
1490
1491     # wss is disabled until all platforms support pyOpenSSL.
1492     # close WEBSOCKETSECURESERVER_IN;
1493     # close WEBSOCKETSECURESERVER_OUT;
1494     # close WEBSOCKETSECURESERVER_ERR;
1495     # kill 15, $webSocketSecureServerPID;
1496
1497     $isWebSocketServerOpen = 0;
1498 }
1499
1500 sub fileNameWithNumber($$)
1501 {
1502     my ($base, $number) = @_;
1503     return "$base$number" if ($number > 1);
1504     return $base;
1505 }
1506
1507 sub processIgnoreTests($$)
1508 {
1509     my @ignoreList = split(/\s*,\s*/, shift);
1510     my $listName = shift;
1511
1512     my $disabledSuffix = "-disabled";
1513
1514     my $addIgnoredDirectories = sub {
1515         return () if exists $ignoredLocalDirectories{basename($File::Find::dir)};
1516         $ignoredDirectories{File::Spec->abs2rel($File::Find::dir, $testDirectory)} = 1;
1517         return @_;
1518     };
1519     foreach my $item (@ignoreList) {
1520         my $path = catfile($testDirectory, $item); 
1521         if (-d $path) {
1522             $ignoredDirectories{$item} = 1;
1523             find({ preprocess => $addIgnoredDirectories, wanted => sub {} }, $path);
1524         }
1525         elsif (-f $path) {
1526             $ignoredFiles{$item} = 1;
1527         } elsif (-f $path . $disabledSuffix) {
1528             # The test is disabled, so do nothing.
1529         } else {
1530             print "$listName list contained '$item', but no file of that name could be found\n";
1531         }
1532     }
1533 }
1534
1535 sub stripExtension($)
1536 {
1537     my ($test) = @_;
1538
1539     $test =~ s/\.[a-zA-Z]+$//;
1540     return $test;
1541 }
1542
1543 sub isTextOnlyTest($)
1544 {
1545     my ($actual) = @_;
1546     my $isText;
1547     if ($actual =~ /^layer at/ms) {
1548         $isText = 0;
1549     } else {
1550         $isText = 1;
1551     }
1552     return $isText;
1553 }
1554
1555 sub expectedDirectoryForTest($;$;$)
1556 {
1557     my ($base, $isText, $expectedExtension) = @_;
1558
1559     my @directories = @platformResultHierarchy;
1560     push @directories, map { catdir($platformBaseDirectory, $_) } qw(mac-snowleopard mac) if isCygwin();
1561     push @directories, $expectedDirectory;
1562
1563     # If we already have expected results, just return their location.
1564     foreach my $directory (@directories) {
1565         return $directory if (-f "$directory/$base-$expectedTag.$expectedExtension");
1566     }
1567
1568     # For cross-platform tests, text-only results should go in the cross-platform directory,
1569     # while render tree dumps should go in the least-specific platform directory.
1570     return $isText ? $expectedDirectory : $platformResultHierarchy[$#platformResultHierarchy];
1571 }
1572
1573 sub countFinishedTest($$$$)
1574 {
1575     my ($test, $base, $result, $isText) = @_;
1576
1577     if (($count + 1) % $testsPerDumpTool == 0 || $count == $#tests) {
1578         if ($shouldCheckLeaks) {
1579             my $fileName;
1580             if ($testsPerDumpTool == 1) {
1581                 $fileName = "$testResultsDirectory/$base-leaks.txt";
1582             } else {
1583                 $fileName = "$testResultsDirectory/" . fileNameWithNumber($dumpToolName, $leaksOutputFileNumber) . "-leaks.txt";
1584             }
1585             my $leakCount = countAndPrintLeaks($dumpToolName, $dumpToolPID, $fileName);
1586             $totalLeaks += $leakCount;
1587             $leaksOutputFileNumber++ if ($leakCount);
1588         }
1589
1590         closeDumpTool();
1591     }
1592     
1593     $count++;
1594     $counts{$result}++;
1595     push @{$tests{$result}}, $test;
1596 }
1597
1598 sub testCrashedOrTimedOut($$$$$)
1599 {
1600     my ($test, $base, $didCrash, $actual, $error) = @_;
1601
1602     printFailureMessageForTest($test, $didCrash ? "crashed" : "timed out");
1603
1604     sampleDumpTool() unless $didCrash;
1605
1606     my $dir = "$testResultsDirectory/$base";
1607     $dir =~ s|/([^/]+)$|| or die "Failed to find test name from base\n";
1608     mkpath $dir;
1609
1610     deleteExpectedAndActualResults($base);
1611
1612     if (defined($error) && length($error)) {
1613         writeToFile("$testResultsDirectory/$base-$errorTag.txt", $error);
1614     }
1615
1616     recordActualResultsAndDiff($base, $actual);
1617
1618     kill 9, $dumpToolPID unless $didCrash;
1619
1620     closeDumpTool();
1621 }
1622
1623 sub printFailureMessageForTest($$)
1624 {
1625     my ($test, $description) = @_;
1626
1627     unless ($verbose) {
1628         print "\n" unless $atLineStart;
1629         print "$test -> ";
1630     }
1631     print "$description\n";
1632     $atLineStart = 1;
1633 }
1634
1635 my %cygpaths = ();
1636
1637 sub openCygpathIfNeeded($)
1638 {
1639     my ($options) = @_;
1640
1641     return unless isCygwin();
1642     return $cygpaths{$options} if $cygpaths{$options} && $cygpaths{$options}->{"open"};
1643
1644     local (*CYGPATHIN, *CYGPATHOUT);
1645     my $pid = open2(\*CYGPATHIN, \*CYGPATHOUT, "cygpath -f - $options");
1646     my $cygpath =  {
1647         "pid" => $pid,
1648         "in" => *CYGPATHIN,
1649         "out" => *CYGPATHOUT,
1650         "open" => 1
1651     };
1652
1653     $cygpaths{$options} = $cygpath;
1654
1655     return $cygpath;
1656 }
1657
1658 sub closeCygpaths()
1659 {
1660     return unless isCygwin();
1661
1662     foreach my $cygpath (values(%cygpaths)) {
1663         close $cygpath->{"in"};
1664         close $cygpath->{"out"};
1665         waitpid($cygpath->{"pid"}, 0);
1666         $cygpath->{"open"} = 0;
1667
1668     }
1669 }
1670
1671 sub convertPathUsingCygpath($$)
1672 {
1673     my ($path, $options) = @_;
1674
1675     my $cygpath = openCygpathIfNeeded($options);
1676     local *inFH = $cygpath->{"in"};
1677     local *outFH = $cygpath->{"out"};
1678     print outFH $path . "\n";
1679     chomp(my $convertedPath = <inFH>);
1680     return $convertedPath;
1681 }
1682
1683 sub toWindowsPath($)
1684 {
1685     my ($path) = @_;
1686     return unless isCygwin();
1687
1688     return convertPathUsingCygpath($path, "-w");
1689 }
1690
1691 sub toURL($)
1692 {
1693     my ($path) = @_;
1694
1695     if ($useRemoteLinksToTests) {
1696         my $relativePath = File::Spec->abs2rel($path, $testDirectory);
1697
1698         # If the file is below the test directory then convert it into a link to the file in SVN
1699         if ($relativePath !~ /^\.\.\//) {
1700             my $revision = svnRevisionForDirectory($testDirectory);
1701             my $svnPath = pathRelativeToSVNRepositoryRootForPath($path);
1702             return "http://trac.webkit.org/export/$revision/$svnPath";
1703         }
1704     }
1705
1706     return $path unless isCygwin();
1707
1708     return "file:///" . convertPathUsingCygpath($path, "-m");
1709 }
1710
1711 sub validateSkippedArg($$;$)
1712 {
1713     my ($option, $value, $value2) = @_;
1714     my %validSkippedValues = map { $_ => 1 } qw(default ignore only);
1715     $value = lc($value);
1716     die "Invalid argument '" . $value . "' for option $option" unless $validSkippedValues{$value};
1717     $treatSkipped = $value;
1718 }
1719
1720 sub htmlForResultsSection(\@$&)
1721 {
1722     my ($tests, $description, $linkGetter) = @_;
1723
1724     my @html = ();
1725     return join("\n", @html) unless @{$tests};
1726
1727     push @html, "<p>$description:</p>";
1728     push @html, "<table>";
1729     foreach my $test (@{$tests}) {
1730         push @html, "<tr>";
1731         push @html, "<td><a href=\"" . toURL("$testDirectory/$test") . "\">$test</a></td>";
1732         foreach my $link (@{&{$linkGetter}($test)}) {
1733             push @html, "<td><a href=\"$link->{href}\">$link->{text}</a></td>";
1734         }
1735         push @html, "</tr>";
1736     }
1737     push @html, "</table>";
1738
1739     return join("\n", @html);
1740 }
1741
1742 sub linksForExpectedAndActualResults($)
1743 {
1744     my ($base) = @_;
1745
1746     my @links = ();
1747
1748     return \@links unless -s "$testResultsDirectory/$base-$diffsTag.txt";
1749     
1750     my $expectedResultPath = $expectedResultPaths{$base};
1751     my ($expectedResultFileName, $expectedResultsDirectory, $expectedResultExtension) = fileparse($expectedResultPath, qr{\.[^.]+$});
1752
1753     push @links, { href => "$base-$expectedTag$expectedResultExtension", text => "expected" };
1754     push @links, { href => "$base-$actualTag$expectedResultExtension", text => "actual" };
1755     push @links, { href => "$base-$diffsTag.txt", text => "diff" };
1756     push @links, { href => "$base-$prettyDiffTag.html", text => "pretty diff" };
1757
1758     return \@links;
1759 }
1760
1761 sub linksForMismatchTest
1762 {
1763     my ($test) = @_;
1764
1765     my @links = ();
1766
1767     my $base = stripExtension($test);
1768
1769     push @links, @{linksForExpectedAndActualResults($base)};
1770     return \@links unless $pixelTests && $imagesPresent{$base};
1771
1772     push @links, { href => "$base-$expectedTag.png", text => "expected image" };
1773     push @links, { href => "$base-$diffsTag.html", text => "image diffs" };
1774     push @links, { href => "$base-$diffsTag.png", text => "$imageDifferences{$base}%" };
1775
1776     return \@links;
1777 }
1778
1779 sub linksForErrorTest
1780 {
1781     my ($test) = @_;
1782
1783     my @links = ();
1784
1785     my $base = stripExtension($test);
1786
1787     push @links, @{linksForExpectedAndActualResults($base)};
1788     push @links, { href => "$base-$errorTag.txt", text => "stderr" };
1789
1790     return \@links;
1791 }
1792
1793 sub linksForNewTest
1794 {
1795     my ($test) = @_;
1796
1797     my @links = ();
1798
1799     my $base = stripExtension($test);
1800
1801     my $expectedResultPath = $expectedResultPaths{$base};
1802     my ($expectedResultFileName, $expectedResultsDirectory, $expectedResultExtension) = fileparse($expectedResultPath, qr{\.[^.]+$});
1803
1804     push @links, { href => "$base-$actualTag$expectedResultExtension", text => "result" };
1805     if ($pixelTests && $imagesPresent{$base}) {
1806         push @links, { href => "$base-$expectedTag.png", text => "image" };
1807     }
1808
1809     return \@links;
1810 }
1811
1812 sub deleteExpectedAndActualResults($)
1813 {
1814     my ($base) = @_;
1815
1816     unlink "$testResultsDirectory/$base-$actualTag.txt";
1817     unlink "$testResultsDirectory/$base-$diffsTag.txt";
1818     unlink "$testResultsDirectory/$base-$errorTag.txt";
1819 }
1820
1821 sub recordActualResultsAndDiff($$)
1822 {
1823     my ($base, $actualResults) = @_;
1824
1825     return unless defined($actualResults) && length($actualResults);
1826
1827     my $expectedResultPath = $expectedResultPaths{$base};
1828     my ($expectedResultFileNameMinusExtension, $expectedResultDirectoryPath, $expectedResultExtension) = fileparse($expectedResultPath, qr{\.[^.]+$});
1829     my $actualResultsPath = "$testResultsDirectory/$base-$actualTag$expectedResultExtension";
1830     my $copiedExpectedResultsPath = "$testResultsDirectory/$base-$expectedTag$expectedResultExtension";
1831
1832     mkpath(dirname($actualResultsPath));
1833     writeToFile("$actualResultsPath", $actualResults);
1834
1835     if (-f $expectedResultPath) {
1836         copy("$expectedResultPath", "$copiedExpectedResultsPath");
1837     } else {
1838         open EMPTY, ">$copiedExpectedResultsPath";
1839         close EMPTY;
1840     }
1841
1842     my $diffOuputBasePath = "$testResultsDirectory/$base";
1843     my $diffOutputPath = "$diffOuputBasePath-$diffsTag.txt";
1844     system "diff -u \"$copiedExpectedResultsPath\" \"$actualResultsPath\" > \"$diffOutputPath\"";
1845
1846     my $prettyDiffOutputPath = "$diffOuputBasePath-$prettyDiffTag.html";
1847     my $prettyPatchPath = "BugsSite/PrettyPatch/";
1848     my $prettifyPath = "$prettyPatchPath/prettify.rb";
1849     system "ruby -I \"$prettyPatchPath\" \"$prettifyPath\" \"$diffOutputPath\" > \"$prettyDiffOutputPath\"";
1850 }
1851
1852 sub buildPlatformResultHierarchy()
1853 {
1854     mkpath($platformTestDirectory) if ($platform eq "undefined" && !-d "$platformTestDirectory");
1855
1856     my @platforms;
1857     if ($platform =~ /^mac-/) {
1858         my $i;
1859         for ($i = 0; $i < @macPlatforms; $i++) {
1860             last if $macPlatforms[$i] eq $platform;
1861         }
1862         for (; $i < @macPlatforms; $i++) {
1863             push @platforms, $macPlatforms[$i];
1864         }
1865     } elsif ($platform =~ /^qt-/) {
1866         push @platforms, $platform;
1867         push @platforms, "qt";
1868     } else {
1869         @platforms = $platform;
1870     }
1871
1872     my @hierarchy;
1873     for (my $i = 0; $i < @platforms; $i++) {
1874         my $scoped = catdir($platformBaseDirectory, $platforms[$i]);
1875         push(@hierarchy, $scoped) if (-d $scoped);
1876     }
1877
1878     return @hierarchy;
1879 }
1880
1881 sub buildPlatformTestHierarchy(@)
1882 {
1883     my (@platformHierarchy) = @_;
1884     return @platformHierarchy if (@platformHierarchy < 2);
1885
1886     return ($platformHierarchy[0], $platformHierarchy[$#platformHierarchy]);
1887 }
1888
1889 sub epiloguesAndPrologues($$)
1890 {
1891     my ($lastDirectory, $directory) = @_;
1892     my @lastComponents = split('/', $lastDirectory);
1893     my @components = split('/', $directory);
1894
1895     while (@lastComponents) {
1896         if (!defined($components[0]) || $lastComponents[0] ne $components[0]) {
1897             last;
1898         }
1899         shift @components;
1900         shift @lastComponents;
1901     }
1902
1903     my @result;
1904     my $leaving = $lastDirectory;
1905     foreach (@lastComponents) {
1906         my $epilogue = $leaving . "/resources/run-webkit-tests-epilogue.html";
1907         foreach (@platformResultHierarchy) {
1908             push @result, catdir($_, $epilogue) if (stat(catdir($_, $epilogue)));
1909         }
1910         push @result, catdir($testDirectory, $epilogue) if (stat(catdir($testDirectory, $epilogue)));
1911         $leaving =~ s|(^\|/)[^/]+$||;
1912     }
1913
1914     my $entering = $leaving;
1915     foreach (@components) {
1916         $entering .= '/' . $_;
1917         my $prologue = $entering . "/resources/run-webkit-tests-prologue.html";
1918         push @result, catdir($testDirectory, $prologue) if (stat(catdir($testDirectory, $prologue)));
1919         foreach (reverse @platformResultHierarchy) {
1920             push @result, catdir($_, $prologue) if (stat(catdir($_, $prologue)));
1921         }
1922     }
1923     return @result;
1924 }
1925     
1926 sub parseLeaksandPrintUniqueLeaks()
1927 {
1928     return unless @leaksFilenames;
1929      
1930     my $mergedFilenames = join " ", @leaksFilenames;
1931     my $parseMallocHistoryTool = sourceDir() . "/WebKitTools/Scripts/parse-malloc-history";
1932     
1933     open MERGED_LEAKS, "cat $mergedFilenames | $parseMallocHistoryTool --merge-depth $mergeDepth  - |" ;
1934     my @leakLines = <MERGED_LEAKS>;
1935     close MERGED_LEAKS;
1936     
1937     my $uniqueLeakCount = 0;
1938     my $totalBytes;
1939     foreach my $line (@leakLines) {
1940         ++$uniqueLeakCount if ($line =~ /^(\d*)\scalls/);
1941         $totalBytes = $1 if $line =~ /^total\:\s(.*)\s\(/;
1942     }
1943     
1944     print "\nWARNING: $totalLeaks total leaks found for a total of $totalBytes!\n";
1945     print "WARNING: $uniqueLeakCount unique leaks found!\n";
1946     print "See above for individual leaks results.\n" if ($leaksOutputFileNumber > 2);
1947     
1948 }
1949
1950 sub extensionForMimeType($)
1951 {
1952     my ($mimeType) = @_;
1953
1954     if ($mimeType eq "application/x-webarchive") {
1955         return "webarchive";
1956     } elsif ($mimeType eq "application/pdf") {
1957         return "pdf";
1958     }
1959     return "txt";
1960 }
1961
1962 # Read up to the first #EOF (the content block of the test), or until detecting crashes or timeouts.
1963 sub readFromDumpToolWithTimer(**)
1964 {
1965     my ($fhIn, $fhError) = @_;
1966
1967     setFileHandleNonBlocking($fhIn, 1);
1968     setFileHandleNonBlocking($fhError, 1);
1969
1970     my $maximumSecondsWithoutOutput = $timeoutSeconds;
1971     $maximumSecondsWithoutOutput *= 10 if $guardMalloc;
1972     my $microsecondsToWaitBeforeReadingAgain = 1000;
1973
1974     my $timeOfLastSuccessfulRead = time;
1975
1976     my @output = ();
1977     my @error = ();
1978     my $status = "success";
1979     my $mimeType = "text/plain";
1980     # We don't have a very good way to know when the "headers" stop
1981     # and the content starts, so we use this as a hack:
1982     my $haveSeenContentType = 0;
1983     my $haveSeenEofIn = 0;
1984     my $haveSeenEofError = 0;
1985
1986     while (1) {
1987         if (time - $timeOfLastSuccessfulRead > $maximumSecondsWithoutOutput) {
1988             $status = dumpToolDidCrash() ? "crashed" : "timedOut";
1989             last;
1990         }
1991
1992         # Once we've seen the EOF, we must not read anymore.
1993         my $lineIn = readline($fhIn) unless $haveSeenEofIn;
1994         my $lineError = readline($fhError) unless $haveSeenEofError;
1995         if (!defined($lineIn) && !defined($lineError)) {
1996             last if ($haveSeenEofIn && $haveSeenEofError);
1997
1998             if ($! != EAGAIN) {
1999                 $status = "crashed";
2000                 last;
2001             }
2002
2003             # No data ready
2004             usleep($microsecondsToWaitBeforeReadingAgain);
2005             next;
2006         }
2007
2008         $timeOfLastSuccessfulRead = time;
2009
2010         if (defined($lineIn)) {
2011             if (!$haveSeenContentType && $lineIn =~ /^Content-Type: (\S+)$/) {
2012                 $mimeType = $1;
2013                 $haveSeenContentType = 1;
2014             } elsif ($lineIn =~ /#EOF/) {
2015                 $haveSeenEofIn = 1;
2016             } else {
2017                 push @output, $lineIn;
2018             }
2019         }
2020         if (defined($lineError)) {
2021             if ($lineError =~ /#EOF/) {
2022                 $haveSeenEofError = 1;
2023             } else {
2024                 push @error, $lineError;
2025             }
2026         }
2027     }
2028
2029     setFileHandleNonBlocking($fhIn, 0);
2030     setFileHandleNonBlocking($fhError, 0);
2031     return {
2032         output => join("", @output),
2033         error => join("", @error),
2034         status => $status,
2035         mimeType => $mimeType,
2036         extension => extensionForMimeType($mimeType)
2037     };
2038 }
2039
2040 sub setFileHandleNonBlocking(*$)
2041 {
2042     my ($fh, $nonBlocking) = @_;
2043
2044     my $flags = fcntl($fh, F_GETFL, 0) or die "Couldn't get filehandle flags";
2045
2046     if ($nonBlocking) {
2047         $flags |= O_NONBLOCK;
2048     } else {
2049         $flags &= ~O_NONBLOCK;
2050     }
2051
2052     fcntl($fh, F_SETFL, $flags) or die "Couldn't set filehandle flags";
2053
2054     return 1;
2055 }
2056
2057 sub sampleDumpTool()
2058 {
2059     return unless isAppleMacWebKit();
2060     return unless $runSample;
2061
2062     my $outputDirectory = "$ENV{HOME}/Library/Logs/DumpRenderTree";
2063     -d $outputDirectory or mkdir $outputDirectory;
2064
2065     my $outputFile = "$outputDirectory/HangReport.txt";
2066     system "/usr/bin/sample", $dumpToolPID, qw(10 10 -file), $outputFile;
2067 }
2068
2069 sub stripMetrics($$)
2070 {
2071     my ($actual, $expected) = @_;
2072
2073     foreach my $result ($actual, $expected) {
2074         $result =~ s/at \(-?[0-9]+,-?[0-9]+\) *//g;
2075         $result =~ s/size -?[0-9]+x-?[0-9]+ *//g;
2076         $result =~ s/text run width -?[0-9]+: //g;
2077         $result =~ s/text run width -?[0-9]+ [a-zA-Z ]+: //g;
2078         $result =~ s/RenderButton {BUTTON} .*/RenderButton {BUTTON}/g;
2079         $result =~ s/RenderImage {INPUT} .*/RenderImage {INPUT}/g;
2080         $result =~ s/RenderBlock {INPUT} .*/RenderBlock {INPUT}/g;
2081         $result =~ s/RenderTextControl {INPUT} .*/RenderTextControl {INPUT}/g;
2082         $result =~ s/\([0-9]+px/px/g;
2083         $result =~ s/ *" *\n +" */ /g;
2084         $result =~ s/" +$/"/g;
2085
2086         $result =~ s/- /-/g;
2087         $result =~ s/\n( *)"\s+/\n$1"/g;
2088         $result =~ s/\s+"\n/"\n/g;
2089         $result =~ s/scrollWidth [0-9]+/scrollWidth/g;
2090         $result =~ s/scrollHeight [0-9]+/scrollHeight/g;
2091     }
2092
2093     return ($actual, $expected);
2094 }
2095
2096 sub fileShouldBeIgnored
2097 {
2098     my ($filePath) = @_;
2099     foreach my $ignoredDir (keys %ignoredDirectories) {
2100         if ($filePath =~ m/^$ignoredDir/) {
2101             return 1;
2102         }
2103     }
2104     return 0;
2105 }
2106
2107 sub readSkippedFiles($)
2108 {
2109     my ($constraintPath) = @_;
2110
2111     foreach my $level (@platformTestHierarchy) {
2112         if (open SKIPPED, "<", "$level/Skipped") {
2113             if ($verbose) {
2114                 my ($dir, $name) = splitpath($level);
2115                 print "Skipped tests in $name:\n";
2116             }
2117
2118             while (<SKIPPED>) {
2119                 my $skipped = $_;
2120                 chomp $skipped;
2121                 $skipped =~ s/^[ \n\r]+//;
2122                 $skipped =~ s/[ \n\r]+$//;
2123                 if ($skipped && $skipped !~ /^#/) {
2124                     if ($skippedOnly) {
2125                         if (!fileShouldBeIgnored($skipped)) {
2126                             if (!$constraintPath) {
2127                                 # Always add $skipped since no constraint path was specified on the command line.
2128                                 push(@ARGV, $skipped);
2129                             } elsif ($skipped =~ /^($constraintPath)/) {
2130                                 # Add $skipped only if it matches the current path constraint, e.g.,
2131                                 # "--skipped=only dir1" with "dir1/file1.html" on the skipped list.
2132                                 push(@ARGV, $skipped);
2133                             } elsif ($constraintPath =~ /^($skipped)/) {
2134                                 # Add current path constraint if it is more specific than the skip list entry,
2135                                 # e.g., "--skipped=only dir1/dir2/dir3" with "dir1" on the skipped list.
2136                                 push(@ARGV, $constraintPath);
2137                             }
2138                         } elsif ($verbose) {
2139                             print "    $skipped\n";
2140                         }
2141                     } else {
2142                         if ($verbose) {
2143                             print "    $skipped\n";
2144                         }
2145                         processIgnoreTests($skipped, "Skipped");
2146                     }
2147                 }
2148             }
2149             close SKIPPED;
2150         }
2151     }
2152 }
2153
2154 my @testsToRun;
2155
2156 sub directoryFilter
2157 {
2158     return () if exists $ignoredLocalDirectories{basename($File::Find::dir)};
2159     return () if exists $ignoredDirectories{File::Spec->abs2rel($File::Find::dir, $testDirectory)};
2160     return @_;
2161 }
2162
2163 sub fileFilter
2164 {
2165     my $filename = $_;
2166     if ($filename =~ /\.([^.]+)$/) {
2167         if (exists $supportedFileExtensions{$1}) {
2168             my $path = File::Spec->abs2rel(catfile($File::Find::dir, $filename), $testDirectory);
2169             push @testsToRun, $path if !exists $ignoredFiles{$path};
2170         }
2171     }
2172 }
2173
2174 sub findTestsToRun
2175 {
2176     @testsToRun = ();
2177
2178     for my $test (@ARGV) {
2179         $test =~ s/^($layoutTestsName|$testDirectory)\///;
2180         my $fullPath = catfile($testDirectory, $test);
2181         if (file_name_is_absolute($test)) {
2182             print "can't run test $test outside $testDirectory\n";
2183         } elsif (-f $fullPath) {
2184             my ($filename, $pathname, $fileExtension) = fileparse($test, qr{\.[^.]+$});
2185             if (!exists $supportedFileExtensions{substr($fileExtension, 1)}) {
2186                 print "test $test does not have a supported extension\n";
2187             } elsif ($testHTTP || $pathname !~ /^http\//) {
2188                 push @testsToRun, $test;
2189             }
2190         } elsif (-d $fullPath) {
2191             find({ preprocess => \&directoryFilter, wanted => \&fileFilter }, $fullPath);
2192             for my $level (@platformTestHierarchy) {
2193                 my $platformPath = catfile($level, $test);
2194                 find({ preprocess => \&directoryFilter, wanted => \&fileFilter }, $platformPath) if (-d $platformPath);
2195             }
2196         } else {
2197             print "test $test not found\n";
2198         }
2199     }
2200
2201     if (!scalar @ARGV) {
2202         find({ preprocess => \&directoryFilter, wanted => \&fileFilter }, $testDirectory);
2203         for my $level (@platformTestHierarchy) {
2204             find({ preprocess => \&directoryFilter, wanted => \&fileFilter }, $level);
2205         }
2206     }
2207
2208     # Remove duplicate tests
2209     @testsToRun = keys %{{ map { $_ => 1 } @testsToRun }};
2210
2211     @testsToRun = sort pathcmp @testsToRun;
2212
2213     # Reverse the tests
2214     @testsToRun = reverse @testsToRun if $reverseTests;
2215
2216     # Shuffle the array
2217     @testsToRun = shuffle(@testsToRun) if $randomizeTests;
2218
2219     return @testsToRun;
2220 }
2221
2222 sub printResults
2223 {
2224     my %text = (
2225         match => "succeeded",
2226         mismatch => "had incorrect layout",
2227         new => "were new",
2228         timedout => "timed out",
2229         crash => "crashed",
2230         error => "had stderr output"
2231     );
2232
2233     for my $type ("match", "mismatch", "new", "timedout", "crash", "error") {
2234         my $typeCount = $counts{$type};
2235         next unless $typeCount;
2236         my $typeText = $text{$type};
2237         my $message;
2238         if ($typeCount == 1) {
2239             $typeText =~ s/were/was/;
2240             $message = sprintf "1 test case (%d%%) %s\n", 1 * 100 / $count, $typeText;
2241         } else {
2242             $message = sprintf "%d test cases (%d%%) %s\n", $typeCount, $typeCount * 100 / $count, $typeText;
2243         }
2244         $message =~ s-\(0%\)-(<1%)-;
2245         print $message;
2246     }
2247 }