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