Reviewed by Eric.
[WebKit-https.git] / WebKitTools / Scripts / run-webkit-tests
1 #!/usr/bin/perl
2
3 # Copyright (C) 2005, 2006 Apple Computer, Inc.  All rights reserved.
4 # Copyright (C) 2006 Alexey Proskuryakov (ap@nypop.com)
5 #
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
8 # are met:
9 #
10 # 1.  Redistributions of source code must retain the above copyright
11 #     notice, this list of conditions and the following disclaimer. 
12 # 2.  Redistributions in binary form must reproduce the above copyright
13 #     notice, this list of conditions and the following disclaimer in the
14 #     documentation and/or other materials provided with the distribution. 
15 # 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
16 #     its contributors may be used to endorse or promote products derived
17 #     from this software without specific prior written permission. 
18 #
19 # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
20 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
23 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30 # Script to run the Web Kit Open Source Project layout tests.
31
32 # Run all the tests passed in on the command line.
33 # If no tests are passed, find all the .html, .shtml, .xml, .xhtml, .pl, .php (and svg) files in the test directory.
34
35 # Run each text.
36 # Compare against the existing file xxx-expected.txt.
37 # If there is a mismatch, generate xxx-actual.txt and xxx-diffs.txt.
38
39 # At the end, report:
40 #   the number of tests that got the expected results
41 #   the number of tests that ran, but did not get the expected results
42 #   the number of tests that failed to run
43 #   the number of tests that were run but had no expected results to compare against
44
45 use strict;
46 use warnings;
47
48 use Cwd;
49 use File::Basename;
50 use File::Copy;
51 use File::Find;
52 use File::Path;
53 use File::Spec;
54 use File::Spec::Functions;
55 use FindBin;
56 use Getopt::Long;
57 use IPC::Open2;
58 use Time::HiRes qw(time);
59
60 use lib $FindBin::Bin;
61 use webkitdirs;
62
63 sub closeDumpRenderTree();
64 sub closeHTTPD();
65 sub countAndPrintLeaks($$$);
66 sub fileNameWithNumber($$);
67 sub numericcmp($$);
68 sub openDumpRenderTreeIfNeeded();
69 sub openHTTPDIfNeeded();
70 sub pathcmp($$);
71 sub processIgnoreTests($);
72 sub slowestcmp($$);
73 sub splitpath($);
74
75 # Argument handling
76 my $checkLeaks = '';
77 my $guardMalloc = '';
78 my $httpdPort = 8000;
79 my $ignoreTests = '';
80 my $launchSafari = 1;
81 my $pixelTests = '';
82 my $quiet = '';
83 my $repaintSweepHorizontally = '';
84 my $repaintTests = '';
85 my $report10Slowest = 0;
86 my $resetResults = 0;
87 my $showHelp = 0;
88 my $singly = 0;
89 my $testHTTP = 1;
90 my $testOnlySVGs = '';
91 my $testResultsDirectory = "/tmp/layout-test-results";
92 my $verbose = 0;
93
94 my $expectedTag = "expected";
95 if (isCygwin()) {
96     $expectedTag = "expected-win";
97 } elsif (isQt()) {
98     $expectedTag = "expected-qt";
99 }
100
101 my $actualTag = "actual";
102 if (isCygwin()) {
103     $actualTag = "actual-win";
104 } elsif (isQt()) {
105     $actualTag = "actual-qt";
106 }
107
108 my $diffsTag = "diffs";
109 if (isCygwin()) {
110     $actualTag = "diffs-win";
111 } elsif (isQt()) {
112     $actualTag = "diffs-qt";
113 }
114
115 my $usage =
116     "Usage: " . basename($0) . " [options] [testdir|testpath ...]\n" .
117     "  -g|--guard-malloc       Enable malloc guard\n" .
118     "  --help                  Show this help message\n" .
119     "  -h|--horizontal-sweep   Change repaint to sweep horizontally instead of vertically (implies --repaint-tests)\n" .
120     "  --[no-]http             Run (or do not run) http tests (default: " . ($testHTTP ? "run" : "do not run") . ")\n" .
121     "  -i|--ignore-tests       Comma-separated list of directories or tests to ignore\n" .
122     "  --[no-]launch-safari    Launch (or do not launch) Safari to display test results (default: "
123         . ($launchSafari ? "launch" : "do not launch") . ")\n" .
124     "  -l|--leaks              Enable leaks checking\n" .
125     "  -p|--pixel-tests        Enable pixel tests\n" .
126     "  --port                  Web server port to use with http tests\n" .
127     "  -q|--quiet              Less verbose output\n" .
128     "  -r|--repaint-tests      Run repaint tests (implies --pixel-tests)\n" .
129     "  --reset-results         Reset ALL results (including pixel tests if --pixel-tests is set)\n" .
130     "  -o|--results-directory  Output results directory (default: " . $testResultsDirectory . ")\n" .
131     "  -1|--singly             Isolate each test case run (implies --verbose)\n" .
132     "  --slowest               Report the 10 slowest tests\n" .
133     "  --svg                   Run only SVG tests (implies --pixel-tests)\n" .
134     "  -v|--verbose            More verbose output (overrides --quiet)\n" .
135     "  --debug|--release       Set DumpRenderTree build configuration\n";
136
137 # Parse out build options (--debug or --release) first
138 my $buildConfiguration = setConfiguration();
139
140 my $getOptionsResult = GetOptions(
141     'guard-malloc|g' => \$guardMalloc,
142     'help' => \$showHelp,
143     'horizontal-sweep|h' => \$repaintSweepHorizontally,
144     'http!' => \$testHTTP,
145     'ignore-tests|i=s' => \$ignoreTests,
146     'launch-safari!' => \$launchSafari,
147     'leaks|l' => \$checkLeaks,
148     'pixel-tests|p' => \$pixelTests,
149     'port=i' => \$httpdPort,
150     'quiet|q' => \$quiet,
151     'repaint-tests|r' => \$repaintTests,
152     'reset-results' => \$resetResults,
153     'results-directory|o=s' => \$testResultsDirectory,
154     'singly|1' => \$singly,
155     'slowest' => \$report10Slowest,
156     'svg' => \$testOnlySVGs, 
157     'verbose|v' => \$verbose,
158 );
159
160 if (!$getOptionsResult || $showHelp) {
161     print STDERR $usage;
162     exit 1;
163 }
164
165 $repaintTests = 1 if $repaintSweepHorizontally;
166
167 $pixelTests = 1 if $testOnlySVGs;
168 $pixelTests = 1 if $repaintTests;
169
170 $verbose = 1 if $singly;
171
172 # Force --no-http for Qt/Linux, for now.
173 $testHTTP = 0 if isQt();
174
175 my $productDir = productDir();
176 $productDir .= "/WebKitTools/DumpRenderTree/DumpRenderTree.qtproj" if (isQt());
177
178 chdirWebKit();
179
180 my @buildOptions = ();
181 if ($buildConfiguration) {
182     push @buildOptions, '--' . lc($buildConfiguration);
183 }
184 my $buildResult = system "WebKitTools/Scripts/build-dumprendertree", @buildOptions;
185 exit WEXITSTATUS($buildResult) if $buildResult;
186
187 my $dumpToolName = "DumpRenderTree";
188 my $tool = "$productDir/$dumpToolName";
189 die "can't find executable $dumpToolName (looked in $productDir)\n" if !-x $tool;
190
191 my $imageDiffTool = "$productDir/ImageDiff";
192 die "can't find executable $imageDiffTool (looked in $productDir)\n" if $pixelTests && !-x $imageDiffTool;
193
194 checkFrameworks();
195
196 my $layoutTestsName = $testOnlySVGs ? "LayoutTests/svg" : "LayoutTests";
197 my $testDirectory = File::Spec->rel2abs($layoutTestsName);
198 my $expectedTestResultsDirectory = $testDirectory;
199 $expectedTestResultsDirectory = $ENV{"WebKitExpectedTestResultsDirectory"} if $ENV{"WebKitExpectedTestResultsDirectory"};
200
201 my $testResults = catfile($testResultsDirectory, "results.html");
202
203 print "Running tests from $testDirectory\n";
204
205 my @tests = ();
206
207 my %ignoredFiles = ();
208 my %ignoredDirectories = ();
209 my %ignoredLocalDirectories = map { $_ => 1 } qw(.svn _svn resources);
210 my %supportedFileExtensions = map { $_ => 1 } qw(html shtml xml xhtml pl php);
211 if ($testOnlySVGs) {
212     %supportedFileExtensions = map { $_ => 1 } qw(svg xml);
213 } elsif (checkWebCoreSVGSupport($testOnlySVGs)) { 
214     $supportedFileExtensions{'svg'} = 1;
215 } else {
216     $ignoredLocalDirectories{'svg'} = 1;
217 }
218 if (!$testHTTP) {
219     $ignoredDirectories{'http'} = 1;
220 }
221
222 if ($ignoreTests) {
223     processIgnoreTests($ignoreTests);
224 }
225
226 my $directoryFilter = sub {
227     return () if exists $ignoredLocalDirectories{basename($File::Find::dir)};
228     return () if exists $ignoredDirectories{File::Spec->abs2rel($File::Find::dir, $testDirectory)};
229     return @_;
230 };
231
232 my $fileFilter = sub {
233     my $filename = $_;
234     if ($filename =~ /\.([^.]+)$/) {
235         if (exists $supportedFileExtensions{$1}) {
236             my $path = File::Spec->abs2rel(catfile($File::Find::dir, $filename), $testDirectory);
237             push @tests, $path if !exists $ignoredFiles{$path};
238         }
239     }
240 };
241
242 for my $test (@ARGV) {
243     $test =~ s/^($layoutTestsName|$testDirectory)\///;
244     my $fullPath = catfile($testDirectory, $test);
245     if (file_name_is_absolute($test)) {
246         print "can't run test $test outside $testDirectory\n";
247     } elsif (-f $fullPath) {
248         my ($filename, $pathname, $fileExtension) = fileparse($test, qr{\.[^.]+$});
249         if (!exists $supportedFileExtensions{substr($fileExtension, 1)}) {
250             print "test $test does not have a supported extension\n";
251         } elsif ($testHTTP || $pathname !~ /^http\//) {
252             push @tests, $test;
253         }
254     } elsif (-d $fullPath) {
255         find({ preprocess => $directoryFilter, wanted => $fileFilter }, $fullPath);
256     } else {
257         print "test $test not found\n";
258     }
259 }
260 if (!scalar @ARGV) {
261     find({ preprocess => $directoryFilter, wanted => $fileFilter }, $testDirectory);
262 }
263
264 die "no tests to run\n" if !@tests;
265
266 @tests = sort pathcmp @tests;
267
268 my %counts;
269 my %tests;
270 my %imagesPresent;
271 my %durations;
272 my $count = 0;
273 my $maxTestsPerLeaksRun = 1000; # more than 3000 and malloc logging will normally run out of memory
274 my $leaksOutputFileNumber = 1;
275 my $totalLeaks = 0;
276
277 my @toolArgs = ();
278 push @toolArgs, "--dump-all-pixels" if $pixelTests && $resetResults;
279 push @toolArgs, "--pixel-tests" if $pixelTests;
280 push @toolArgs, "--repaint" if $repaintTests;
281 push @toolArgs, "--horizontal-sweep" if $repaintSweepHorizontally;
282 push @toolArgs, "-";
283
284 $| = 1;
285
286 my $imageDiffToolPID;
287 if ($pixelTests) {
288     local %ENV;
289     $ENV{MallocStackLogging} = 1 if $checkLeaks;
290     $imageDiffToolPID = open2(\*DIFFIN, \*DIFFOUT, $imageDiffTool, "") or die "unable to open $imageDiffTool\n";
291 }
292
293 my $dumpToolPID;
294 my $toolOpen = 0;
295
296 my $atLineStart = 1;
297 my $lastDirectory = "";
298
299 my $httpdOpen = 0;
300
301 print "Testing ", scalar @tests, " test cases.\n";
302 my $overallStartTime = time;
303
304 for my $test (@tests) {
305     next if $test eq 'results.html';
306
307     openDumpRenderTreeIfNeeded();
308
309     my $base = $test;
310     $base =~ s/\.[a-zA-Z]+$//;
311     
312     if ($verbose) {
313         print "running $test -> ";
314         $atLineStart = 0;
315     } elsif (!$quiet) {
316         my $dir = $base;
317         $dir =~ s|/[^/]+$||;
318         if ($dir ne $lastDirectory) {
319             print "\n" unless $atLineStart;
320             print "$dir ";
321             $lastDirectory = $dir;
322         }
323         print ".";
324         $atLineStart = 0;
325     }
326
327     my $result;
328
329     my $startTime = time if $report10Slowest;
330
331     if ($test !~ /^http\//) {
332         my $testPath = "$testDirectory/$test";
333         if (isCygwin()) {
334             $testPath = `cygpath -m -s "$testPath"`;
335         }
336         else {
337             $testPath = canonpath($testPath);
338         }
339         print OUT "$testPath\n";
340     } else {
341         openHTTPDIfNeeded();
342         
343         my $path = canonpath($test);
344         $path =~ s/^http\/tests\///;
345         print OUT "http://127.0.0.1:$httpdPort/$path\n";
346     }
347
348     my $actual = "";
349     while (<IN>) {
350         last if /#EOF/;
351         $actual .= $_;
352     }
353
354     $durations{$test} = time - $startTime if $report10Slowest;
355
356     my $expected;
357     if (!$resetResults && open EXPECTED, "<", "$expectedTestResultsDirectory/$base-$expectedTag.txt") {
358         $expected = "";
359         while (<EXPECTED>) {
360             $expected .= $_;
361         }
362         close EXPECTED;
363     }
364
365     if ($checkLeaks && $singly) {
366         print "        $test -> ";
367     }
368
369     my $actualPNG = "";
370     my $diffPNG = "";
371     my $diffPercentage = "";
372     my $diffResult = "passed";
373     
374     if ($pixelTests) {
375         die "Pixel tests currently don't work when using a custom expected test results directory" if $testDirectory ne $expectedTestResultsDirectory;
376             
377         my $actualHash = "";
378         my $expectedHash = "";
379         my $actualPNGSize = 0;
380         while (<IN>) {
381             last if /#EOF/;
382             if (/ActualHash: ([a-f0-9]{32})/) {
383                 $actualHash = $1;
384             } elsif (/BaselineHash: ([a-f0-9]{32})/) {
385                 $expectedHash = $1;
386             } elsif (/Content-length: (\d+)\s*/) {
387                 $actualPNGSize = $1;
388                 read(IN, $actualPNG, $actualPNGSize);
389             }
390         }
391
392         if ($expectedHash ne $actualHash && -f "$testDirectory/$base-$expectedTag.png") {
393             my $expectedPNGSize = -s "$testDirectory/$base-$expectedTag.png";
394             my $expectedPNG = "";
395             open EXPECTEDPNG, "$testDirectory/$base-$expectedTag.png";
396             read(EXPECTEDPNG, $expectedPNG, $expectedPNGSize);
397
398             print DIFFOUT "Content-length: $actualPNGSize\n";
399             print DIFFOUT $actualPNG;
400
401             print DIFFOUT "Content-length: $expectedPNGSize\n";
402             print DIFFOUT $expectedPNG;
403
404             while (<DIFFIN>) {
405                 last if /^error/ || /^diff:/;
406                 if (/Content-length: (\d+)\s*/) {
407                     read(DIFFIN, $diffPNG, $1);
408                 }
409             }
410
411             if (/^diff: (.+)% (passed|failed)/) {
412                 $diffPercentage = $1;
413                 $diffResult = $2;
414             }
415         }
416
417         if ($actualPNGSize && ($resetResults || !-f "$testDirectory/$base-$expectedTag.png")) {
418             open EXPECTED, ">", "$testDirectory/$base-expected.png" or die "could not create $testDirectory/$base-expected.png\n";
419             print EXPECTED $actualPNG;
420             close EXPECTED;
421         }
422
423         # update the expected hash if the image diff said that there was no difference
424         if ($actualHash ne "" && ($resetResults || !-f "$testDirectory/$base-$expectedTag.checksum")) {
425             open EXPECTED, ">", "$testDirectory/$base-$expectedTag.checksum" or die "could not create $testDirectory/$base-$expectedTag.checksum\n";
426             print EXPECTED $actualHash;
427             close EXPECTED;
428         }
429     }
430
431     if (!defined $expected) {
432         if ($verbose) {
433             print "new " . ($resetResults ? "result" : "test") ."\n";
434             $atLineStart = 1;
435         }
436         $result = "new";
437         
438         # Create the path if needed
439         mkpath(catfile($expectedTestResultsDirectory, dirname($base))) if $testDirectory ne $expectedTestResultsDirectory;
440         
441         open EXPECTED, ">", "$expectedTestResultsDirectory/$base-$expectedTag.txt" or die "could not create $expectedTestResultsDirectory/$base-$expectedTag.txt\n";
442         print EXPECTED $actual;
443         close EXPECTED;
444         unlink "$testResultsDirectory/$base-$actualTag.txt";
445         unlink "$testResultsDirectory/$base-$diffsTag.txt";
446     } elsif ($actual eq $expected && $diffResult eq "passed") {
447         if ($verbose) {
448             print "succeeded\n";
449             $atLineStart = 1;
450         }
451         $result = "match";
452         unlink "$testResultsDirectory/$base-$actualTag.txt";
453         unlink "$testResultsDirectory/$base-$diffsTag.txt";
454     } else {
455         unless ($verbose) {
456             print "\n" unless $atLineStart;
457             print "$test -> ";
458         }
459         print "failed\n";
460         $atLineStart = 1;
461
462         $result = "mismatch";
463
464         my $dir = "$testResultsDirectory/$base";
465         $dir =~ s|/([^/]+)$|| or die "Failed to find test name from base\n";
466         my $testName = $1;
467         mkpath $dir;
468
469         open ACTUAL, ">", "$testResultsDirectory/$base-$actualTag.txt" or die;
470         print ACTUAL $actual;
471         close ACTUAL;
472
473         system "diff -u \"$expectedTestResultsDirectory/$base-$expectedTag.txt\" \"$testResultsDirectory/$base-$actualTag.txt\" > \"$testResultsDirectory/$base-$diffsTag.txt\"";
474
475         if ($pixelTests && $diffPNG && $diffPNG ne "") {
476             $imagesPresent{$base} = 1;
477
478             open ACTUAL, ">", "$testResultsDirectory/$base-$actualTag.png" or die;
479             print ACTUAL $actualPNG;
480             close ACTUAL;
481
482             open DIFF, ">", "$testResultsDirectory/$base-$diffsTag.png" or die;
483             print DIFF $diffPNG;
484             close DIFF;
485             
486             copy("$expectedTestResultsDirectory/$base-$expectedTag.png", "$testResultsDirectory/$base-$expectedTag.png");
487
488             open DIFFHTML, ">$testResultsDirectory/$base-$diffsTag.html" or die;
489             print DIFFHTML "<html>\n";
490             print DIFFHTML "<head>\n";
491             print DIFFHTML "<title>$base Image Compare</title>\n";
492             print DIFFHTML "<script language=\"Javascript\" type=\"text/javascript\">\n";
493             print DIFFHTML "var currentImage = 0;\n";
494             print DIFFHTML "var imageNames = new Array(\"Actual\", \"Expected\");\n";
495             print DIFFHTML "var imagePaths = new Array(\"$testName-$actualTag.png\", \"$testName-$expectedTag.png\");\n";
496             if (-f "$testDirectory/$base-w3c.png") {
497                 copy("$testDirectory/$base-w3c.png", "$testResultsDirectory/$base-w3c.png");
498                 print DIFFHTML "imageNames.push(\"W3C\");\n";
499                 print DIFFHTML "imagePaths.push(\"$testName-w3c.png\");\n";
500             }
501             print DIFFHTML "function animateImage() {\n";
502             print DIFFHTML "    var image = document.getElementById(\"animatedImage\");\n";
503             print DIFFHTML "    var imageText = document.getElementById(\"imageText\");\n";
504             print DIFFHTML "    image.src = imagePaths[currentImage];\n";
505             print DIFFHTML "    imageText.innerHTML = imageNames[currentImage] + \" Image\";\n";
506             print DIFFHTML "    currentImage = (currentImage + 1) % imageNames.length;\n";
507             print DIFFHTML "    setTimeout('animateImage()',2000);\n";
508             print DIFFHTML "}\n";
509             print DIFFHTML "</script>\n";
510             print DIFFHTML "</head>\n";
511             print DIFFHTML "<body onLoad=\"animateImage();\">\n";
512             print DIFFHTML "<table>\n";
513             if ($diffPercentage) {
514                 print DIFFHTML "<tr>\n";
515                 print DIFFHTML "<td>Difference between images: <a href=\"$testName-$diffsTag.png\">$diffPercentage%</a></td>\n";
516                 print DIFFHTML "</tr>\n";
517             }
518             print DIFFHTML "<tr>\n";
519             print DIFFHTML "<td id=\"imageText\" style=\"text-weight: bold;\">Actual Image</td>\n";
520             print DIFFHTML "</tr>\n";
521             print DIFFHTML "<tr>\n";
522             print DIFFHTML "<td><img src=\"$testName-$actualTag.png\" id=\"animatedImage\"></td>\n";
523             print DIFFHTML "</tr>\n";
524             print DIFFHTML "</table>\n";
525             print DIFFHTML "</body>\n";
526             print DIFFHTML "</html>\n";
527         }
528     }
529
530     if ($checkLeaks && $toolOpen) {
531         if ($singly) {
532             $totalLeaks += countAndPrintLeaks($dumpToolName, $dumpToolPID, "$testResultsDirectory/$base-leaks.txt");
533         } elsif ($count && (($count % $maxTestsPerLeaksRun) == 0)) {
534             my $leaksFileName = fileNameWithNumber($dumpToolName, $leaksOutputFileNumber);
535             my $leaksCount = countAndPrintLeaks($dumpToolName, $dumpToolPID, "$testResultsDirectory/$leaksFileName-leaks.txt");
536             $leaksOutputFileNumber++ if ($leaksCount);
537             $totalLeaks += $leaksCount;
538             closeDumpRenderTree();
539         }
540     }
541
542     if ($singly && $toolOpen) {
543         closeDumpRenderTree();
544     }
545
546     $count += 1;
547     $counts{$result} += 1;
548     push @{$tests{$result}}, $test;
549 }
550 printf "\n%0.2fs total testing time\n", (time - $overallStartTime) . "";
551
552 closeHTTPD();
553
554 if ($checkLeaks && !$singly && $toolOpen) {
555     my $leaksFileName = fileNameWithNumber($dumpToolName, $leaksOutputFileNumber);
556     $totalLeaks += countAndPrintLeaks($dumpToolName, $dumpToolPID, "$testResultsDirectory/$leaksFileName-leaks.txt");
557     $leaksOutputFileNumber++;
558 }
559
560 # FIXME: Do we really want to check the image-comparison tool for leaks every time?
561 if ($checkLeaks && $pixelTests) {
562     $totalLeaks += countAndPrintLeaks("ImageDiff", $imageDiffToolPID, "$testResultsDirectory/ImageDiff-leaks.txt");
563 }
564
565 if ($totalLeaks) {
566     print "\nWARNING: $totalLeaks total leaks found!\n";
567     print "See above for individual leaks results.\n" if ($leaksOutputFileNumber > 2);
568 }
569
570 close IN;
571 close OUT;
572
573 if ($report10Slowest) {
574     print "\n\nThe 10 slowest tests:\n\n";
575     my $count = 0;
576     for my $test (sort slowestcmp keys %durations) {
577         printf "%0.2f secs: %s\n", $durations{$test}, $test;
578         last if ++$count == 10;
579     }
580 }
581
582 print "\n";
583
584 if ($resetResults || ($counts{match} && $counts{match} == $count)) {
585     print "all $count test cases succeeded\n";
586     unlink $testResults;
587     exit;
588 }
589
590 my %text = (
591     match => "succeeded",
592     mismatch => "had incorrect layout",
593     new => "were new",
594     fail => "failed (tool did not execute successfully)",
595 );
596
597 for my $type ("match", "mismatch", "new", "fail") {
598     my $c = $counts{$type};
599     if ($c) {
600         my $t = $text{$type};
601         my $message;
602         if ($c == 1) {
603             $t =~ s/were/was/;
604             $message = sprintf "1 test case (%d%%) %s\n", 1 * 100 / $count, $t;
605         } else {
606             $message = sprintf "%d test cases (%d%%) %s\n", $c, $c * 100 / $count, $t;
607         }
608         $message =~ s-\(0%\)-(<1%)-;
609         print $message;
610     }
611 }
612
613 mkpath $testResultsDirectory;
614
615 open HTML, ">", $testResults or die;
616 print HTML "<html>\n";
617 print HTML "<head>\n";
618 print HTML "<title>Layout Test Results</title>\n";
619 print HTML "</head>\n";
620 print HTML "<body>\n";
621
622 if ($counts{mismatch}) {
623     print HTML "<p>Tests where results did not match expected results:</p>\n";
624     print HTML "<table>\n";
625     for my $test (@{$tests{mismatch}}) {
626         my $base = $test;
627         $base =~ s/\.[a-zA-Z]+$//;
628         copy("$expectedTestResultsDirectory/$base-$expectedTag.txt", "$testResultsDirectory/$base-$expectedTag.txt");
629         print HTML "<tr>\n";            
630         print HTML "<td><a href=\"$testDirectory/$test\">$base</a></td>\n";
631         if (-s "$testResultsDirectory/$base-$diffsTag.txt") {
632             print HTML "<td><a href=\"$base-$expectedTag.txt\">expected</a></td>\n";
633             print HTML "<td><a href=\"$base-$actualTag.txt\">actual</a></td>\n";
634             print HTML "<td><a href=\"$base-$diffsTag.txt\">diffs</a></td>\n";
635         } else {
636             print HTML "<td></td><td></td><td></td>\n";
637         }
638         if ($pixelTests) {
639             if ($imagesPresent{$base}) {
640                 print HTML "<td><a href=\"$base-$expectedTag.png\">expected image</a></td>\n";
641                 print HTML "<td><a href=\"$base-$diffsTag.html\">image diffs</a></td>\n";
642             } else {
643                 print HTML "<td></td><td></td>\n";
644             }            
645         }
646         print HTML "</tr>\n";
647     }
648     print HTML "</table>\n";
649 }
650
651 if ($counts{fail}) {
652     print HTML "<p>Tests that caused the DumpRenderTree tool to fail:</p>\n";
653     print HTML "<table>\n";
654     for my $test (@{$tests{fail}}) {
655         my $base = $test;
656         $base =~ s/\.[a-zA-Z]+$//;
657         print HTML "<tr>\n";
658         print HTML "<td><a href=\"$testDirectory/$test\">$base</a></td>\n";
659         print HTML "</tr>\n";
660     }
661     print HTML "</table>\n";
662 }
663
664 if ($counts{new}) {
665     print HTML "<p>Tests that had no expected results (probably new):</p>\n";
666     print HTML "<table>\n";
667     for my $test (@{$tests{new}}) {
668         my $base = $test;
669         $base =~ s/\.[a-zA-Z]+$//;
670         print HTML "<tr>\n";
671         print HTML "<td><a href=\"$expectedTestResultsDirectory/$test\">$base</a></td>\n";
672         print HTML "<td><a href=\"$expectedTestResultsDirectory/$base-$expectedTag.txt\">results</a></td>\n";
673         if ($pixelTests && -f "$expectedTestResultsDirectory/$base-$expectedTag.png") {
674             print HTML "<td><a href=\"$expectedTestResultsDirectory/$base-$expectedTag.png\">image</a></td>\n";
675         }
676         print HTML "</tr>\n";
677     }
678     print HTML "</table>\n";
679 }
680
681 print HTML "</body>\n";
682 print HTML "</html>\n";
683 close HTML;
684
685 system "WebKitTools/Scripts/run-safari", "-NSOpen", $testResults if $launchSafari;
686
687 exit 1;
688
689 sub countAndPrintLeaks($$$)
690 {
691     my ($toolName, $toolPID, $leaksFilePath) = @_;
692
693     print "\n" unless $atLineStart;
694     $atLineStart = 1;
695
696     # We are excluding the following reported leaks so they don't get in our way when looking for WebKit leaks:
697
698     my @exclude = (
699         "pthread_create", # false positive leak of 'THRD', Radar 3387783
700         "_CFPreferencesDomainDeepCopyDictionary", # leak apparently in CFPreferences, Radar 4220786
701         "FOGetCoveredUnicodeChars", # leak apparently in ATS, Radar 3943604
702         "PCFragPrepareClosureFromFile", # leak in Code Fragment Manager, Radar 3426998
703         "Flash_EnforceLocalSecurity", # leaks in flash plugin code, Radar 4449747
704         "ICCFPrefWrapper::GetPrefDictionary()" # leaks in quicktime plugin code, Radar 4449794
705     );
706
707     # Note that the exclusion for bug 3387783 doesn't quite work right; sometimes a leak of 'THRD' will
708     # still appear in the leaks output.
709
710     my $excludes = "-exclude '" . (join "' -exclude '", @exclude) . "'";
711
712     print " ? checking for leaks in $toolName\n";
713     my $leaksOutput = `leaks $excludes $toolPID`;
714     my ($count, $bytes) = $leaksOutput =~ /Process $toolPID: (\d+) leaks? for (\d+) total/;
715     my ($excluded) = $leaksOutput =~ /(\d+) leaks? excluded/;
716
717     my $adjustedCount = $count;
718     $adjustedCount -= $excluded if $excluded;
719
720     if (!$adjustedCount) {
721         print " - no leaks found\n";
722         unlink $leaksFilePath;
723         return 0;
724     } else {
725         my $dir = $leaksFilePath;
726         $dir =~ s|/[^/]+$|| or die;
727         mkpath $dir;
728
729         if ($excluded) {
730             print " + $adjustedCount leaks ($bytes bytes including $excluded excluded leaks) were found, details in $leaksFilePath\n";
731         } else {
732             print " + $count leaks ($bytes bytes) were found, details in $leaksFilePath\n";
733         }
734
735         open LEAKS, ">", $leaksFilePath or die;
736         print LEAKS $leaksOutput;
737         close LEAKS;
738     }
739
740     return $adjustedCount;
741 }
742
743 # Break up a path into the directory (with slash) and base name.
744 sub splitpath($)
745 {
746     my ($path) = @_;
747
748     my $pathSeparator = "/";
749     my $dirname = dirname($path) . $pathSeparator;
750     $dirname = "" if $dirname eq "." . $pathSeparator;
751
752     return ($dirname, basename($path));
753 }
754
755 # Sort first by directory, then by file, so all paths in one directory are grouped
756 # rather than being interspersed with items from subdirectories.
757 # Use numericcmp to sort directory and filenames to make order logical.
758 sub pathcmp($$)
759 {
760     my ($patha, $pathb) = @_;
761
762     my ($dira, $namea) = splitpath($patha);
763     my ($dirb, $nameb) = splitpath($pathb);
764
765     return numericcmp($dira, $dirb) if $dira ne $dirb;
766     return numericcmp($namea, $nameb);
767 }
768
769 # Sort numeric parts of strings as numbers, other parts as strings.
770 # Makes 1.33 come after 1.3, which is cool.
771 sub numericcmp($$)
772 {
773     my ($aa, $bb) = @_;
774
775     my @a = split /(\d+)/, $aa;
776     my @b = split /(\d+)/, $bb;
777
778     # Compare one chunk at a time.
779     # Each chunk is either all numeric digits, or all not numeric digits.
780     while (@a && @b) {
781         my $a = shift @a;
782         my $b = shift @b;
783         
784         # Use numeric comparison if chunks are non-equal numbers.
785         return $a <=> $b if $a =~ /^\d/ && $b =~ /^\d/ && $a != $b;
786
787         # Use string comparison if chunks are any other kind of non-equal string.
788         return $a cmp $b if $a ne $b;
789     }
790     
791     # One of the two is now empty; compare lengths for result in this case.
792     return @a <=> @b;
793 }
794
795 # Sort slowest tests first.
796 sub slowestcmp($$)
797 {
798     my ($testa, $testb) = @_;
799
800     my $dura = $durations{$testa};
801     my $durb = $durations{$testb};
802     return $durb <=> $dura if $dura != $durb;
803     return pathcmp($testa, $testb);
804 }
805
806 sub openDumpRenderTreeIfNeeded()
807 {
808     return if $toolOpen;
809
810     # Save some requires variables for the linux environment...
811     my $homeDir = $ENV{'HOME'};
812     my $dbusAddress = $ENV{'DBUS_SESSION_BUS_ADDRESS'};
813
814     local %ENV;
815     $ENV{DISPLAY} = ":0" if isQt();
816     $ENV{HOME} = $homeDir if isQt();
817     $ENV{DBUS_SESSION_BUS_ADDRESS} = $dbusAddress if isQt();
818     $ENV{DYLD_FRAMEWORK_PATH} = $productDir;
819     $ENV{XML_CATALOG_FILES} = ""; # work around missing /etc/catalog <rdar://problem/4292995>
820     $ENV{MallocStackLogging} = 1 if $checkLeaks;
821     $ENV{DYLD_INSERT_LIBRARIES} = "/usr/lib/libgmalloc.dylib" if $guardMalloc;
822     $dumpToolPID = open2(\*IN, \*OUT, $tool, @toolArgs) or die "Failed to start tool: $tool\n";
823     $toolOpen = 1;
824 }
825
826 sub closeDumpRenderTree()
827 {
828     return if !$toolOpen;
829
830     close IN;
831     close OUT;
832     waitpid $dumpToolPID, 0;
833     $toolOpen = 0;
834 }
835
836 sub openHTTPDIfNeeded()
837 {
838     return if $httpdOpen;
839
840     mkdir "/tmp/WebKit";
841     
842     if (-f "/tmp/WebKit/httpd.pid") {
843         my $oldPid = `cat /tmp/WebKit/httpd.pid`;
844         chomp $oldPid;
845         if (0 != kill 0, $oldPid) {
846             print "\nhttpd is already running: pid $oldPid, killing...\n";
847             kill 15, $oldPid;
848             
849             my $retryCount = 20;
850             while ((0 != kill 0, $oldPid) && $retryCount) {
851                 sleep 1;
852                 --$retryCount;
853             }
854             
855             die "Timed out waiting for httpd to quit" unless $retryCount;
856         }
857     }
858     
859     my $httpdPath = "/usr/sbin/httpd";
860     my $httpdConfig = "$testDirectory/http/conf/httpd.conf";
861     $httpdConfig = "$testDirectory/http/conf/apache2-httpd.conf" if `$httpdPath -v` =~ m|Apache/2|;
862     my $documentRoot = "$testDirectory/http/tests";
863     my $typesConfig = "$testDirectory/http/conf/mime.types";
864     my $listen = "127.0.0.1:$httpdPort";
865     my $absTestResultsDirectory = File::Spec->rel2abs(glob $testResultsDirectory);
866
867     mkpath $absTestResultsDirectory;
868
869     open2(\*HTTPDIN, \*HTTPDOUT, $httpdPath, 
870         "-f", "$httpdConfig",
871         "-C", "DocumentRoot \"$documentRoot\"",
872         "-C", "Listen $listen",
873         "-c", "TypesConfig \"$typesConfig\"",
874         "-c", "CustomLog \"$absTestResultsDirectory/access_log.txt\" common",
875         "-c", "ErrorLog \"$absTestResultsDirectory/error_log.txt\"",
876         # Apache wouldn't run CGIs with permissions==700 otherwise
877         "-c", "User \"#$<\"");
878
879     my $retryCount = 20;
880     while (system("/usr/bin/curl -q --silent --stderr - --output /dev/null $listen") && $retryCount) {
881         sleep 1;
882         --$retryCount;
883     }
884     
885     die "Timed out waiting for httpd to start" unless $retryCount;
886     
887     $httpdOpen = 1;
888 }
889
890 sub closeHTTPD()
891 {
892     return if !$httpdOpen;
893
894     close HTTPDIN;
895     close HTTPDOUT;
896
897     kill 15, `cat /tmp/WebKit/httpd.pid` if -f "/tmp/WebKit/httpd.pid";
898
899     $httpdOpen = 0;
900 }
901
902 sub fileNameWithNumber($$)
903 {
904     my ($base, $number) = @_;
905     return "$base$number" if ($number > 1);
906     return $base;
907 }
908
909 sub processIgnoreTests($) {
910     my @ignoreList = split(/\s*,\s*/, shift);
911     my $addIgnoredDirectories = sub {
912         return () if exists $ignoredLocalDirectories{basename($File::Find::dir)};
913         $ignoredDirectories{File::Spec->abs2rel($File::Find::dir, $testDirectory)} = 1;
914         return @_;
915     };
916     foreach my $item (@ignoreList) {
917         my $path = catfile($testDirectory, $item); 
918         if (-d $path) {
919             $ignoredDirectories{$item} = 1;
920             find({ preprocess => $addIgnoredDirectories, wanted => sub {} }, $path);
921         }
922         elsif (-f $path) {
923             $ignoredFiles{$item} = 1;
924         }
925         else {
926             print "ignoring '$item' on ignore-tests list\n";
927         }
928     }
929 }