d769721792d20732edd828962395a1894d330c85
[WebKit-https.git] / WebKitTools / Scripts / run-webkit-tests
1 #!/usr/bin/perl
2
3 # Copyright (C) 2005, 2006, 2007 Apple 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 use POSIX;
63
64 sub openDumpTool();
65 sub closeDumpTool();
66 sub closeHTTPD();
67 sub countAndPrintLeaks($$$);
68 sub fileNameWithNumber($$);
69 sub numericcmp($$);
70 sub openHTTPDIfNeeded();
71 sub pathcmp($$);
72 sub processIgnoreTests($);
73 sub slowestcmp($$);
74 sub splitpath($);
75 sub isTextOnlyTest($);
76 sub expectedDirectoryForTest($);
77
78 # Argument handling
79 my $shouldCheckLeaks = '';
80 my $guardMalloc = '';
81 my $httpdPort = 8000;
82 my $ignoreTests = '';
83 my $launchSafari = 1;
84 my $pixelTests = '';
85 my $quiet = '';
86 my $repaintSweepHorizontally = '';
87 my $repaintTests = '';
88 my $report10Slowest = 0;
89 my $resetResults = 0;
90 my $showHelp = 0;
91 my $testsPerDumpTool = 1000;
92 my $testHTTP = 1;
93 my $testOnlySVGs = '';
94 my $testResultsDirectory = "/tmp/layout-test-results";
95 my $threaded = 0;
96 my $verbose = 0;
97 my $useValgrind = 0;
98 my $strictTesting = 0;
99 my $generateNewResults = 1;
100
101 my $expectedTag = "expected";
102 my $actualTag = "actual";
103 my $diffsTag = "diffs";
104
105 my $usage =
106     "Usage: " . basename($0) . " [options] [testdir|testpath ...]\n" .
107     "  -g|--guard-malloc       Enable malloc guard\n" .
108     "  --help                  Show this help message\n" .
109     "  -h|--horizontal-sweep   Change repaint to sweep horizontally instead of vertically (implies --repaint-tests)\n" .
110     "  --[no-]http             Run (or do not run) http tests (default: " . ($testHTTP ? "run" : "do not run") . ")\n" .
111     "  -i|--ignore-tests       Comma-separated list of directories or tests to ignore\n" .
112     "  --[no-]launch-safari    Launch (or do not launch) Safari to display test results (default: "
113         . ($launchSafari ? "launch" : "do not launch") . ")\n" .
114     "  -l|--leaks              Enable leaks checking\n" .
115     "  -p|--pixel-tests        Enable pixel tests\n" .
116     "  --port                  Web server port to use with http tests\n" .
117     "  -q|--quiet              Less verbose output\n" .
118     "  -r|--repaint-tests      Run repaint tests (implies --pixel-tests)\n" .
119     "  --reset-results         Reset ALL results (including pixel tests if --pixel-tests is set)\n" .
120     "  --[no-]new-test-results Generate results for new tests\n" .
121     "  -o|--results-directory  Output results directory (default: " . $testResultsDirectory . ")\n" .
122     "  -1|--singly             Isolate each test case run (implies --verbose)\n" .
123     "  --slowest               Report the 10 slowest tests\n" .
124     "  --svg                   Run only SVG tests (implies --pixel-tests)\n" .
125     "  -t|--threaded           Run a concurrent JavaScript thead with each test\n" .
126     "  -v|--verbose            More verbose output (overrides --quiet)\n" .
127     "  --debug|--release       Set DumpRenderTree build configuration\n" .
128     "  --valgrind              Run DumpRenderTree inside valgrind (Qt/Linux only)\n" .
129     "  --strict                Do a comparison with the output on Mac (Qt only)\n";
130
131 # Parse out build options (--debug or --release) first
132 setConfiguration();
133 my $configurationOption = "--" . lc configuration();
134
135 my $getOptionsResult = GetOptions(
136     'guard-malloc|g' => \$guardMalloc,
137     'help' => \$showHelp,
138     'horizontal-sweep|h' => \$repaintSweepHorizontally,
139     'http!' => \$testHTTP,
140     'ignore-tests|i=s' => \$ignoreTests,
141     'launch-safari!' => \$launchSafari,
142     'leaks|l' => \$shouldCheckLeaks,
143     'pixel-tests|p' => \$pixelTests,
144     'port=i' => \$httpdPort,
145     'quiet|q' => \$quiet,
146     'repaint-tests|r' => \$repaintTests,
147     'reset-results' => \$resetResults,
148     'new-test-results!' => \$generateNewResults,
149     'results-directory|o=s' => \$testResultsDirectory,
150     'singly|1' => sub { $testsPerDumpTool = 1; },
151     'nthly=i' => \$testsPerDumpTool,
152     'slowest' => \$report10Slowest,
153     'svg' => \$testOnlySVGs, 
154     'threaded|t' => \$threaded,
155     'verbose|v' => \$verbose,
156     'valgrind' => \$useValgrind,
157     'strict' => \$strictTesting,
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 $testsPerDumpTool == 1;
171
172 if ($shouldCheckLeaks && $testsPerDumpTool > 1000) {
173     die "Running more than 1000 tests at a time with MallocStackLogging enabled may cause a crash.\n";
174 }
175
176 # Force --no-http for Qt/Linux, for now.
177 $testHTTP = 0 if isQt();
178
179 my $productDir = productDir();
180 $productDir .= "/WebKitTools/DumpRenderTree/DumpRenderTree.qtproj" if (isQt());
181
182 chdirWebKit();
183
184 my $buildResult = system "WebKitTools/Scripts/build-dumprendertree", $configurationOption;
185 exit WEXITSTATUS($buildResult) if $buildResult;
186
187 my $dumpToolName = "DumpRenderTree";
188 my $dumpTool = "$productDir/$dumpToolName";
189 die "can't find executable $dumpToolName (looked in $productDir)\n" unless -x $dumpTool;
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 = "LayoutTests";
197 my $testDirectory = File::Spec->rel2abs($layoutTestsName);
198 my $expectedDirectory = $testDirectory;
199 my $expectedDirectoryText = $testDirectory;
200 if (isQt()) {
201     $expectedDirectory = "LayoutTestResults/qt";
202 } elsif (isCygwin()) {
203     $expectedDirectory = "LayoutTestResults/win";
204 }
205 $expectedDirectory = $ENV{"WebKitExpectedTestResultsDirectory"} if $ENV{"WebKitExpectedTestResultsDirectory"};
206
207 if ($testOnlySVGs) {
208     $testDirectory .= "/svg";
209     $expectedDirectory .= "/svg";
210     $expectedDirectoryText .= "/svg";
211 }
212
213 my $testResults = catfile($testResultsDirectory, "results.html");
214
215 print "Running tests from $testDirectory\n";
216
217 my @tests = ();
218 my %testType = ();
219
220 system "ln -s $testDirectory /tmp/LayoutTests" unless -x "/tmp/LayoutTests";
221
222 my %ignoredFiles = ();
223 my %ignoredDirectories = ();
224 my %ignoredLocalDirectories = map { $_ => 1 } qw(.svn _svn resources);
225 my %supportedFileExtensions = map { $_ => 1 } qw(html shtml xml xhtml pl php);
226 if ($testOnlySVGs) {
227     %supportedFileExtensions = map { $_ => 1 } qw(svg xml);
228 } elsif (checkWebCoreSVGSupport($testOnlySVGs)) { 
229     $supportedFileExtensions{'svg'} = 1;
230 } else {
231     $ignoredLocalDirectories{'svg'} = 1;
232 }
233 if (!$testHTTP) {
234     $ignoredDirectories{'http'} = 1;
235 }
236
237 if ($ignoreTests) {
238     processIgnoreTests($ignoreTests);
239 }
240
241 if (open SKIPPED, "<", "$expectedDirectory/Skipped") {
242     if ($verbose) {
243         print "Skipped tests:\n";
244     }
245     while (<SKIPPED>) {
246         my $skipped = $_;
247         chomp $skipped;
248         $skipped =~ s/^ +//;
249         $skipped =~ s/ +$//;
250         if ($skipped && $skipped !~ /^#/) {
251             if($verbose) {
252                 print "    $skipped\n";
253             }
254             processIgnoreTests($skipped);
255         }
256     }
257     close SKIPPED;
258 }
259
260
261 my $directoryFilter = sub {
262     return () if exists $ignoredLocalDirectories{basename($File::Find::dir)};
263     return () if exists $ignoredDirectories{File::Spec->abs2rel($File::Find::dir, $testDirectory)};
264     return @_;
265 };
266
267 my $fileFilter = sub {
268     my $filename = $_;
269     if ($filename =~ /\.([^.]+)$/) {
270         if (exists $supportedFileExtensions{$1}) {
271             my $path = File::Spec->abs2rel(catfile($File::Find::dir, $filename), $testDirectory);
272             push @tests, $path if !exists $ignoredFiles{$path};
273         }
274     }
275 };
276
277 for my $test (@ARGV) {
278     $test =~ s/^($layoutTestsName|$testDirectory)\///;
279     my $fullPath = catfile($testDirectory, $test);
280     if (file_name_is_absolute($test)) {
281         print "can't run test $test outside $testDirectory\n";
282     } elsif (-f $fullPath) {
283         my ($filename, $pathname, $fileExtension) = fileparse($test, qr{\.[^.]+$});
284         if (!exists $supportedFileExtensions{substr($fileExtension, 1)}) {
285             print "test $test does not have a supported extension\n";
286         } elsif ($testHTTP || $pathname !~ /^http\//) {
287             push @tests, $test;
288         }
289     } elsif (-d $fullPath) {
290         find({ preprocess => $directoryFilter, wanted => $fileFilter }, $fullPath);
291     } else {
292         print "test $test not found\n";
293     }
294 }
295 if (!scalar @ARGV) {
296     find({ preprocess => $directoryFilter, wanted => $fileFilter }, $testDirectory);
297 }
298
299 die "no tests to run\n" if !@tests;
300
301 @tests = sort pathcmp @tests;
302
303 my %counts;
304 my %tests;
305 my %imagesPresent;
306 my %durations;
307 my $count = 0;
308 my $leaksOutputFileNumber = 1;
309 my $totalLeaks = 0;
310
311 my @toolArgs = ();
312 push @toolArgs, "--dump-all-pixels" if $pixelTests && $resetResults;
313 push @toolArgs, "--pixel-tests" if $pixelTests;
314 push @toolArgs, "--repaint" if $repaintTests;
315 push @toolArgs, "--horizontal-sweep" if $repaintSweepHorizontally;
316 push @toolArgs, "--threaded" if $threaded;
317 push @toolArgs, "-";
318
319 $| = 1;
320
321 my $imageDiffToolPID;
322 if ($pixelTests) {
323     local %ENV;
324     $ENV{MallocStackLogging} = 1 if $shouldCheckLeaks;
325     $imageDiffToolPID = open2(\*DIFFIN, \*DIFFOUT, $imageDiffTool, "") or die "unable to open $imageDiffTool\n";
326 }
327
328 my $dumpToolPID;
329 my $isDumpToolOpen = 0;
330
331 my $atLineStart = 1;
332 my $lastDirectory = "";
333
334 my $isHttpdOpen = 0;
335
336 print "Testing ", scalar @tests, " test cases.\n";
337 my $overallStartTime = time;
338
339 for my $test (@tests) {
340     next if $test eq 'results.html';
341
342     openDumpTool();
343
344     my $base = $test;
345     $base =~ s/\.[a-zA-Z]+$//;
346     
347     if ($verbose) {
348         print "running $test -> ";
349         $atLineStart = 0;
350     } elsif (!$quiet) {
351         my $dir = $base;
352         $dir =~ s|/[^/]+$||;
353         if ($dir ne $lastDirectory) {
354             print "\n" unless $atLineStart;
355             print "$dir ";
356             $lastDirectory = $dir;
357         }
358         print ".";
359         $atLineStart = 0;
360     }
361
362     my $result;
363
364     my $startTime = time if $report10Slowest;
365
366     if ($test !~ /^http\//) {
367         my $testPath = "$testDirectory/$test";
368         if (isCygwin()) {
369             $testPath = `cygpath -m -s "$testPath"`;
370         }
371         else {
372             $testPath = canonpath($testPath);
373         }
374         print OUT "$testPath\n";
375     } else {
376         openHTTPDIfNeeded();
377         
378         my $path = canonpath($test);
379         $path =~ s/^http\/tests\///;
380         print OUT "http://127.0.0.1:$httpdPort/$path\n";
381     }
382
383     my $actual = "";
384     while (<IN>) {
385         last if /#EOF/;
386         $actual .= $_;
387     }
388     my $isText = isTextOnlyTest($actual);
389
390     $durations{$test} = time - $startTime if $report10Slowest;
391
392     my $expected;
393     my $expectedDir = expectedDirectoryForTest($isText);
394     if (!$resetResults && open EXPECTED, "<", "$expectedDir/$base-$expectedTag.txt") {
395         $expected = "";
396         while (<EXPECTED>) {
397             $expected .= $_;
398         }
399         close EXPECTED;
400     }
401     my $expectedMac;
402     if (isQt() && $strictTesting && !$isText) {
403       if (!$resetResults && open EXPECTED, "<", "$expectedDirectoryText/$base-$expectedTag.txt") {
404         $expectedMac = "";
405         while (<EXPECTED>) {
406           $expectedMac .= $_;
407         }
408         close EXPECTED;
409       }
410     }
411
412     if ($shouldCheckLeaks && $testsPerDumpTool == 1) {
413         print "        $test -> ";
414     }
415
416     my $actualPNG = "";
417     my $diffPNG = "";
418     my $diffPercentage = "";
419     my $diffResult = "passed";
420     
421     if ($pixelTests) {
422         die "Pixel tests currently don't work when using a custom expected test results directory" if $testDirectory ne $expectedDir;
423             
424         my $actualHash = "";
425         my $expectedHash = "";
426         my $actualPNGSize = 0;
427         while (<IN>) {
428             last if /#EOF/;
429             if (/ActualHash: ([a-f0-9]{32})/) {
430                 $actualHash = $1;
431             } elsif (/BaselineHash: ([a-f0-9]{32})/) {
432                 $expectedHash = $1;
433             } elsif (/Content-length: (\d+)\s*/) {
434                 $actualPNGSize = $1;
435                 read(IN, $actualPNG, $actualPNGSize);
436             }
437         }
438
439         if ($expectedHash ne $actualHash && -f "$testDirectory/$base-$expectedTag.png") {
440             my $expectedPNGSize = -s "$testDirectory/$base-$expectedTag.png";
441             my $expectedPNG = "";
442             open EXPECTEDPNG, "$testDirectory/$base-$expectedTag.png";
443             read(EXPECTEDPNG, $expectedPNG, $expectedPNGSize);
444
445             print DIFFOUT "Content-length: $actualPNGSize\n";
446             print DIFFOUT $actualPNG;
447
448             print DIFFOUT "Content-length: $expectedPNGSize\n";
449             print DIFFOUT $expectedPNG;
450
451             while (<DIFFIN>) {
452                 last if /^error/ || /^diff:/;
453                 if (/Content-length: (\d+)\s*/) {
454                     read(DIFFIN, $diffPNG, $1);
455                 }
456             }
457
458             if (/^diff: (.+)% (passed|failed)/) {
459                 $diffPercentage = $1;
460                 $diffResult = $2;
461             }
462         }
463
464         if ($actualPNGSize && ($resetResults || !-f "$testDirectory/$base-$expectedTag.png")) {
465             open EXPECTED, ">", "$testDirectory/$base-expected.png" or die "could not create $testDirectory/$base-expected.png\n";
466             print EXPECTED $actualPNG;
467             close EXPECTED;
468         }
469
470         # update the expected hash if the image diff said that there was no difference
471         if ($actualHash ne "" && ($resetResults || !-f "$testDirectory/$base-$expectedTag.checksum")) {
472             open EXPECTED, ">", "$testDirectory/$base-$expectedTag.checksum" or die "could not create $testDirectory/$base-$expectedTag.checksum\n";
473             print EXPECTED $actualHash;
474             close EXPECTED;
475         }
476     }
477
478     if (isQt() && $strictTesting && !$isText) {
479       if (defined $expectedMac) {
480         my $simplified_actual;
481         $simplified_actual = $actual;
482         $simplified_actual =~ s/at \(-?[0-9]+,-?[0-9]+\) *//g;
483         $simplified_actual =~ s/size -?[0-9]+x-?[0-9]+ *//g;
484         $simplified_actual =~ s/text run width -?[0-9]+: //g;
485         $simplified_actual =~ s/\([0-9]+px/px/g;
486         $simplified_actual =~ s/ *" *\n +" */ /g;
487         $simplified_actual =~ s/" +$/"/g;
488
489         $simplified_actual =~ s/- /-/g;
490         $simplified_actual =~ s/\n( *)"\s+/\n$1"/g;
491         $simplified_actual =~ s/\s+"\n/"\n/g;
492         
493         $expectedMac =~ s/at \(-?[0-9]+,-?[0-9]+\) *//g;
494         $expectedMac =~ s/size -?[0-9]+x-?[0-9]+ *//g;
495         $expectedMac =~ s/text run width -?[0-9]+: //g;
496         $expectedMac =~ s/\([0-9]+px/px/g;
497         $expectedMac =~ s/ *" *\n +" */ /g;
498         $expectedMac =~ s/" +$/"/g;
499
500         $expectedMac =~ s/- /-/g;
501         $expectedMac =~ s/\n( *)"\s+/\n$1"/g;
502         $expectedMac =~ s/\s+"\n/"\n/g;
503         
504         if ($simplified_actual ne $expectedMac) {
505           open ACTUAL, ">", "/tmp/actual.txt" or die;
506           print ACTUAL $simplified_actual;
507           close ACTUAL;
508           open ACTUAL, ">", "/tmp/expected.txt" or die;
509           print ACTUAL $expectedMac;
510           close ACTUAL;
511           system "diff -u \"/tmp/expected.txt\" \"/tmp/actual.txt\" > \"/tmp/simplified.diff\"";
512
513           $diffResult = "failed";
514           if($verbose) {
515             print "\n";
516             system "cat /tmp/simplified.diff";
517             print "failed!!!!!";
518           }
519         }
520       }
521     }
522
523     if (!defined $expected) {
524         if ($verbose) {
525             print "new " . ($resetResults ? "result" : "test") ."\n";
526             $atLineStart = 1;
527         }
528         $result = "new";
529
530         if ($generateNewResults || $resetResults) {
531             # Create the path if needed
532             mkpath(catfile($expectedDir, dirname($base))) if $testDirectory ne $expectedDir;
533
534             open EXPECTED, ">", "$expectedDir/$base-$expectedTag.txt" or die "could not create $expectedDir/$base-$expectedTag.txt\n";
535             print EXPECTED $actual;
536             close EXPECTED;
537         }
538         unlink "$testResultsDirectory/$base-$actualTag.txt";
539         unlink "$testResultsDirectory/$base-$diffsTag.txt";
540         unless ($resetResults) {
541             # Always print the file name for new tests, as they will probably need some manual inspection.
542             # in verbose mode we already printed the test case, so no need to do it again.
543             unless ($verbose) {
544                 print "\n" unless $atLineStart;
545                 print "$test -> ";
546             }
547             print "new\n";
548         }
549         $atLineStart = 1;
550     } elsif ($actual eq $expected && $diffResult eq "passed") {
551         if ($verbose) {
552             print "succeeded\n";
553             $atLineStart = 1;
554         }
555         $result = "match";
556         unlink "$testResultsDirectory/$base-$actualTag.txt";
557         unlink "$testResultsDirectory/$base-$diffsTag.txt";
558     } else {
559         unless ($verbose) {
560             print "\n" unless $atLineStart;
561             print "$test -> ";
562         }
563         print "failed\n";
564         $atLineStart = 1;
565
566         $result = "mismatch";
567
568         my $dir = "$testResultsDirectory/$base";
569         $dir =~ s|/([^/]+)$|| or die "Failed to find test name from base\n";
570         my $testName = $1;
571         mkpath $dir;
572
573         open ACTUAL, ">", "$testResultsDirectory/$base-$actualTag.txt" or die;
574         print ACTUAL $actual;
575         close ACTUAL;
576
577         system "diff -u \"$expectedDir/$base-$expectedTag.txt\" \"$testResultsDirectory/$base-$actualTag.txt\" > \"$testResultsDirectory/$base-$diffsTag.txt\"";
578
579         if ($pixelTests && $diffPNG && $diffPNG ne "") {
580             $imagesPresent{$base} = 1;
581
582             open ACTUAL, ">", "$testResultsDirectory/$base-$actualTag.png" or die;
583             print ACTUAL $actualPNG;
584             close ACTUAL;
585
586             open DIFF, ">", "$testResultsDirectory/$base-$diffsTag.png" or die;
587             print DIFF $diffPNG;
588             close DIFF;
589             
590             copy("$expectedDir/$base-$expectedTag.png", "$testResultsDirectory/$base-$expectedTag.png");
591
592             open DIFFHTML, ">$testResultsDirectory/$base-$diffsTag.html" or die;
593             print DIFFHTML "<html>\n";
594             print DIFFHTML "<head>\n";
595             print DIFFHTML "<title>$base Image Compare</title>\n";
596             print DIFFHTML "<script language=\"Javascript\" type=\"text/javascript\">\n";
597             print DIFFHTML "var currentImage = 0;\n";
598             print DIFFHTML "var imageNames = new Array(\"Actual\", \"Expected\");\n";
599             print DIFFHTML "var imagePaths = new Array(\"$testName-$actualTag.png\", \"$testName-$expectedTag.png\");\n";
600             if (-f "$testDirectory/$base-w3c.png") {
601                 copy("$testDirectory/$base-w3c.png", "$testResultsDirectory/$base-w3c.png");
602                 print DIFFHTML "imageNames.push(\"W3C\");\n";
603                 print DIFFHTML "imagePaths.push(\"$testName-w3c.png\");\n";
604             }
605             print DIFFHTML "function animateImage() {\n";
606             print DIFFHTML "    var image = document.getElementById(\"animatedImage\");\n";
607             print DIFFHTML "    var imageText = document.getElementById(\"imageText\");\n";
608             print DIFFHTML "    image.src = imagePaths[currentImage];\n";
609             print DIFFHTML "    imageText.innerHTML = imageNames[currentImage] + \" Image\";\n";
610             print DIFFHTML "    currentImage = (currentImage + 1) % imageNames.length;\n";
611             print DIFFHTML "    setTimeout('animateImage()',2000);\n";
612             print DIFFHTML "}\n";
613             print DIFFHTML "</script>\n";
614             print DIFFHTML "</head>\n";
615             print DIFFHTML "<body onLoad=\"animateImage();\">\n";
616             print DIFFHTML "<table>\n";
617             if ($diffPercentage) {
618                 print DIFFHTML "<tr>\n";
619                 print DIFFHTML "<td>Difference between images: <a href=\"$testName-$diffsTag.png\">$diffPercentage%</a></td>\n";
620                 print DIFFHTML "</tr>\n";
621             }
622             print DIFFHTML "<tr>\n";
623             print DIFFHTML "<td id=\"imageText\" style=\"text-weight: bold;\">Actual Image</td>\n";
624             print DIFFHTML "</tr>\n";
625             print DIFFHTML "<tr>\n";
626             print DIFFHTML "<td><img src=\"$testName-$actualTag.png\" id=\"animatedImage\"></td>\n";
627             print DIFFHTML "</tr>\n";
628             print DIFFHTML "</table>\n";
629             print DIFFHTML "</body>\n";
630             print DIFFHTML "</html>\n";
631         }
632     }
633
634     if (($count + 1) % $testsPerDumpTool == 0 || $count == $#tests) {
635         if ($shouldCheckLeaks) {
636             my $fileName;
637             if ($testsPerDumpTool == 1) {
638                 $fileName = "$testResultsDirectory/$base-leaks.txt";
639             } else {
640                 $fileName = "$testResultsDirectory/" . fileNameWithNumber($dumpToolName, $leaksOutputFileNumber) . "-leaks.txt";
641             }
642             my $leakCount = countAndPrintLeaks($dumpToolName, $dumpToolPID, $fileName);
643             $totalLeaks += $leakCount;
644             $leaksOutputFileNumber++ if ($leakCount);
645         }
646
647         closeDumpTool();
648     }
649
650     $count++;
651     $counts{$result}++;
652     push @{$tests{$result}}, $test;
653     $testType{$test} = $isText;
654 }
655 printf "\n%0.2fs total testing time\n", (time - $overallStartTime) . "";
656
657 !$isDumpToolOpen || die "Failed to close $dumpToolName.\n";
658
659 closeHTTPD();
660
661 system "rm /tmp/LayoutTests";
662
663 # FIXME: Do we really want to check the image-comparison tool for leaks every time?
664 if ($shouldCheckLeaks && $pixelTests) {
665     $totalLeaks += countAndPrintLeaks("ImageDiff", $imageDiffToolPID, "$testResultsDirectory/ImageDiff-leaks.txt");
666 }
667
668 if ($totalLeaks) {
669     print "\nWARNING: $totalLeaks total leaks found!\n";
670     print "See above for individual leaks results.\n" if ($leaksOutputFileNumber > 2);
671 }
672
673 close IN;
674 close OUT;
675
676 if ($report10Slowest) {
677     print "\n\nThe 10 slowest tests:\n\n";
678     my $count = 0;
679     for my $test (sort slowestcmp keys %durations) {
680         printf "%0.2f secs: %s\n", $durations{$test}, $test;
681         last if ++$count == 10;
682     }
683 }
684
685 print "\n";
686
687 if ($resetResults || ($counts{match} && $counts{match} == $count)) {
688     print "all $count test cases succeeded\n";
689     unlink $testResults;
690     exit;
691 }
692
693
694 my %text = (
695     match => "succeeded",
696     mismatch => "had incorrect layout",
697     new => "were new",
698     fail => "failed (tool did not execute successfully)",
699 );
700
701 for my $type ("match", "mismatch", "new", "fail") {
702     my $c = $counts{$type};
703     if ($c) {
704         my $t = $text{$type};
705         my $message;
706         if ($c == 1) {
707             $t =~ s/were/was/;
708             $message = sprintf "1 test case (%d%%) %s\n", 1 * 100 / $count, $t;
709         } else {
710             $message = sprintf "%d test cases (%d%%) %s\n", $c, $c * 100 / $count, $t;
711         }
712         $message =~ s-\(0%\)-(<1%)-;
713         print $message;
714     }
715 }
716
717 mkpath $testResultsDirectory;
718
719 open HTML, ">", $testResults or die;
720 print HTML "<html>\n";
721 print HTML "<head>\n";
722 print HTML "<title>Layout Test Results</title>\n";
723 print HTML "</head>\n";
724 print HTML "<body>\n";
725
726 if ($counts{mismatch}) {
727     print HTML "<p>Tests where results did not match expected results:</p>\n";
728     print HTML "<table>\n";
729     for my $test (@{$tests{mismatch}}) {
730         my $base = $test;
731         $base =~ s/\.[a-zA-Z]+$//;
732         my $expectedDir;
733         if ($testType{$test}) {
734             $expectedDir = $expectedDirectoryText;
735         } else {
736             $expectedDir = $expectedDirectory;
737         }
738         copy("$expectedDir/$base-$expectedTag.txt", "$testResultsDirectory/$base-$expectedTag.txt");
739         print HTML "<tr>\n";            
740         print HTML "<td><a href=\"$testDirectory/$test\">$base</a></td>\n";
741         if (-s "$testResultsDirectory/$base-$diffsTag.txt") {
742             print HTML "<td><a href=\"$base-$expectedTag.txt\">expected</a></td>\n";
743             print HTML "<td><a href=\"$base-$actualTag.txt\">actual</a></td>\n";
744             print HTML "<td><a href=\"$base-$diffsTag.txt\">diffs</a></td>\n";
745         } else {
746             print HTML "<td></td><td></td><td></td>\n";
747         }
748         if ($pixelTests) {
749             if ($imagesPresent{$base}) {
750                 print HTML "<td><a href=\"$base-$expectedTag.png\">expected image</a></td>\n";
751                 print HTML "<td><a href=\"$base-$diffsTag.html\">image diffs</a></td>\n";
752             } else {
753                 print HTML "<td></td><td></td>\n";
754             }            
755         }
756         print HTML "</tr>\n";
757     }
758     print HTML "</table>\n";
759 }
760
761 if ($counts{fail}) {
762     print HTML "<p>Tests that caused the DumpRenderTree tool to fail:</p>\n";
763     print HTML "<table>\n";
764     for my $test (@{$tests{fail}}) {
765         my $base = $test;
766         my $expectedDir;
767         if ($testType{$test}) {
768             $expectedDir = $expectedDirectoryText;
769         } else {
770             $expectedDir = $expectedDirectory;
771         }
772         $base =~ s/\.[a-zA-Z]+$//;
773         print HTML "<tr>\n";
774         print HTML "<td><a href=\"$testDirectory/$test\">$base</a></td>\n";
775         print HTML "</tr>\n";
776     }
777     print HTML "</table>\n";
778 }
779
780 if ($counts{new}) {
781     print HTML "<p>Tests that had no expected results (probably new):</p>\n";
782     print HTML "<table>\n";
783     for my $test (@{$tests{new}}) {
784         my $base = $test;
785         my $expectedDir;
786         if ($testType{$test}) {
787             $expectedDir = $expectedDirectoryText;
788         } else {
789             $expectedDir = $expectedDirectory;
790         }
791         $base =~ s/\.[a-zA-Z]+$//;
792         print HTML "<tr>\n";
793         print HTML "<td><a href=\"$expectedDir/$test\">$base</a></td>\n";
794         print HTML "<td><a href=\"$expectedDir/$base-$expectedTag.txt\">results</a></td>\n";
795         if ($pixelTests && -f "$expectedDir/$base-$expectedTag.png") {
796             print HTML "<td><a href=\"$expectedDir/$base-$expectedTag.png\">image</a></td>\n";
797         }
798         print HTML "</tr>\n";
799     }
800     print HTML "</table>\n";
801 }
802
803 print HTML "</body>\n";
804 print HTML "</html>\n";
805 close HTML;
806
807 if(isQt()) {
808   system "konqueror", $testResults if $launchSafari;
809 } else {
810   system "WebKitTools/Scripts/run-safari", $configurationOption, "-NSOpen", $testResults if $launchSafari;
811 }
812
813 exit 1;
814
815 sub countAndPrintLeaks($$$)
816 {
817     my ($dumpToolName, $dumpToolPID, $leaksFilePath) = @_;
818
819     print "\n" unless $atLineStart;
820     $atLineStart = 1;
821
822     # We are excluding the following reported leaks so they don't get in our way when looking for WebKit leaks:
823
824     my @typesToExclude = (
825         "THRD", # bug in 'leaks', Radar 3387783
826         "DRHT" #ditto (endian little hate i)
827     );
828
829     my @callStacksToExclude = (
830         "_CFPreferencesDomainDeepCopyDictionary", # leak apparently in CFPreferences, Radar 4220786
831         "FOGetCoveredUnicodeChars", # leak apparently in ATS, Radar 3943604
832         "PCFragPrepareClosureFromFile", # leak in Code Fragment Manager, Radar 3426998
833         "Flash_EnforceLocalSecurity", # leaks in flash plugin code, Radar 4449747
834         "ICCFPrefWrapper::GetPrefDictionary", # leaks in quicktime plugin code, Radar 4449794
835         "GetLineDirectionPreference", "InitUnicodeUtilities", # leak apparently in CFNotificationCenterAddObserver, Radar 4964790
836         "CFRunLoopRunSpecific \\| malloc_zone_malloc", # leak in CFRunLoopRunSpecific, Radar 4670839
837         "CFRunLoopRunSpecific \\| CFAllocatorAllocate ", # ditto
838         "_objc_msgForward", # leak apparently in NSSpellChecker, Radar 4965278
839         "WebCore::Selection::toRange", # bug in 'leaks', Radar 4967949
840         "WebCore::SubresourceLoader::create", # bug in 'leaks', Radar 4985806
841         "NSURLCache cachedResponseForRequest", # leak in CFURL cache, Radar 4768430
842         "NSAttributedString\\(NSAttributedStringKitAdditions\\) _initWithDOMRange:", # leak apparently in NSAttributedString, Radar 4970608
843         "NSAttributedString\\(NSAttributedStringKitAdditions\\) dataFromRange:documentAttributes:error:" # leak apparently in NSAttributedString, Radar 4973752
844     );
845
846     # Note that the exclusion for bug 3387783 doesn't quite work right; sometimes a leak of 'THRD' will
847     # still appear in the leaks output.
848
849     my $leaksTool = sourceDir() . "/WebKitTools/Scripts/run-leaks";
850     my $excludeString = "--exclude-callstack '" . (join "' --exclude-callstack '", @callStacksToExclude) . "' " .
851                         "--exclude-type '" . (join "' --exclude-type '", @typesToExclude) . "'";
852
853     print " ? checking for leaks in $dumpToolName\n";
854     my $leaksOutput = `$leaksTool $excludeString $dumpToolPID`;
855     my ($count, $bytes) = $leaksOutput =~ /Process $dumpToolPID: (\d+) leaks? for (\d+) total/;
856     my ($excluded) = $leaksOutput =~ /(\d+) leaks? excluded/;
857
858     my $adjustedCount = $count;
859     $adjustedCount -= $excluded if $excluded;
860
861     if (!$adjustedCount) {
862         print " - no leaks found\n";
863         unlink $leaksFilePath;
864         return 0;
865     } else {
866         my $dir = $leaksFilePath;
867         $dir =~ s|/[^/]+$|| or die;
868         mkpath $dir;
869
870         if ($excluded) {
871             print " + $adjustedCount leaks ($bytes bytes including $excluded excluded leaks) were found, details in $leaksFilePath\n";
872         } else {
873             print " + $count leaks ($bytes bytes) were found, details in $leaksFilePath\n";
874         }
875
876         open LEAKS, ">", $leaksFilePath or die;
877         print LEAKS $leaksOutput;
878         close LEAKS;
879     }
880
881     return $adjustedCount;
882 }
883
884 # Break up a path into the directory (with slash) and base name.
885 sub splitpath($)
886 {
887     my ($path) = @_;
888
889     my $pathSeparator = "/";
890     my $dirname = dirname($path) . $pathSeparator;
891     $dirname = "" if $dirname eq "." . $pathSeparator;
892
893     return ($dirname, basename($path));
894 }
895
896 # Sort first by directory, then by file, so all paths in one directory are grouped
897 # rather than being interspersed with items from subdirectories.
898 # Use numericcmp to sort directory and filenames to make order logical.
899 sub pathcmp($$)
900 {
901     my ($patha, $pathb) = @_;
902
903     my ($dira, $namea) = splitpath($patha);
904     my ($dirb, $nameb) = splitpath($pathb);
905
906     return numericcmp($dira, $dirb) if $dira ne $dirb;
907     return numericcmp($namea, $nameb);
908 }
909
910 # Sort numeric parts of strings as numbers, other parts as strings.
911 # Makes 1.33 come after 1.3, which is cool.
912 sub numericcmp($$)
913 {
914     my ($aa, $bb) = @_;
915
916     my @a = split /(\d+)/, $aa;
917     my @b = split /(\d+)/, $bb;
918
919     # Compare one chunk at a time.
920     # Each chunk is either all numeric digits, or all not numeric digits.
921     while (@a && @b) {
922         my $a = shift @a;
923         my $b = shift @b;
924         
925         # Use numeric comparison if chunks are non-equal numbers.
926         return $a <=> $b if $a =~ /^\d/ && $b =~ /^\d/ && $a != $b;
927
928         # Use string comparison if chunks are any other kind of non-equal string.
929         return $a cmp $b if $a ne $b;
930     }
931     
932     # One of the two is now empty; compare lengths for result in this case.
933     return @a <=> @b;
934 }
935
936 # Sort slowest tests first.
937 sub slowestcmp($$)
938 {
939     my ($testa, $testb) = @_;
940
941     my $dura = $durations{$testa};
942     my $durb = $durations{$testb};
943     return $durb <=> $dura if $dura != $durb;
944     return pathcmp($testa, $testb);
945 }
946
947 sub openDumpTool()
948 {
949     return if $isDumpToolOpen;
950
951     # Save some requires variables for the linux environment...
952     my $homeDir = $ENV{'HOME'};
953     my $libraryPath = $ENV{'LD_LIBRARY_PATH'};
954     my $dbusAddress = $ENV{'DBUS_SESSION_BUS_ADDRESS'};
955     my $display = $ENV{'DISPLAY'};
956
957     my $isQt = isQt();
958     local %ENV;
959     if ($isQt) {
960         if (defined $display) {
961             $ENV{DISPLAY} = $display;
962         } else {
963             $ENV{DISPLAY} = ":1";
964         }
965         $ENV{HOME} = $homeDir;
966         if (defined $libraryPath) {
967             $ENV{LD_LIBRARY_PATH} = $libraryPath;
968         }
969         if (defined $dbusAddress) {
970             $ENV{DBUS_SESSION_BUS_ADDRESS} = $dbusAddress;
971         }
972     }
973     $ENV{DYLD_FRAMEWORK_PATH} = $productDir;
974     $ENV{XML_CATALOG_FILES} = ""; # work around missing /etc/catalog <rdar://problem/4292995>
975     $ENV{MallocStackLogging} = 1 if $shouldCheckLeaks;
976     $ENV{DYLD_INSERT_LIBRARIES} = "/usr/lib/libgmalloc.dylib" if $guardMalloc;
977     my @args = ();
978     if ($useValgrind) {
979       push @args, $dumpTool;
980     }
981     push @args, @toolArgs;
982     if ($useValgrind) {
983       $dumpTool = "valgrind";
984     }
985     $dumpToolPID = open2(\*IN, \*OUT, $dumpTool, @args) or die "Failed to start tool: $dumpTool\n";
986     $isDumpToolOpen = 1;
987 }
988
989 sub closeDumpTool()
990 {
991     return if !$isDumpToolOpen;
992
993     close IN;
994     close OUT;
995     waitpid $dumpToolPID, 0;
996     $isDumpToolOpen = 0;
997 }
998
999 sub openHTTPDIfNeeded()
1000 {
1001     return if $isHttpdOpen;
1002
1003     mkdir "/tmp/WebKit";
1004     
1005     if (-f "/tmp/WebKit/httpd.pid") {
1006         my $oldPid = `cat /tmp/WebKit/httpd.pid`;
1007         chomp $oldPid;
1008         if (0 != kill 0, $oldPid) {
1009             print "\nhttpd is already running: pid $oldPid, killing...\n";
1010             kill 15, $oldPid;
1011             
1012             my $retryCount = 20;
1013             while ((0 != kill 0, $oldPid) && $retryCount) {
1014                 sleep 1;
1015                 --$retryCount;
1016             }
1017             
1018             die "Timed out waiting for httpd to quit" unless $retryCount;
1019         }
1020     }
1021     
1022     my $httpdPath = "/usr/sbin/httpd";
1023     my $httpdConfig = "$testDirectory/http/conf/httpd.conf";
1024     $httpdConfig = "$testDirectory/http/conf/apache2-httpd.conf" if `$httpdPath -v` =~ m|Apache/2|;
1025     my $documentRoot = "$testDirectory/http/tests";
1026     my $typesConfig = "$testDirectory/http/conf/mime.types";
1027     my $listen = "127.0.0.1:$httpdPort";
1028     my $absTestResultsDirectory = File::Spec->rel2abs(glob $testResultsDirectory);
1029
1030     mkpath $absTestResultsDirectory;
1031
1032     open2(\*HTTPDIN, \*HTTPDOUT, $httpdPath, 
1033         "-f", "$httpdConfig",
1034         "-C", "DocumentRoot \"$documentRoot\"",
1035         "-C", "Listen $listen",
1036         "-c", "TypesConfig \"$typesConfig\"",
1037         "-c", "CustomLog \"$absTestResultsDirectory/access_log.txt\" common",
1038         "-c", "ErrorLog \"$absTestResultsDirectory/error_log.txt\"",
1039         # Apache wouldn't run CGIs with permissions==700 otherwise
1040         "-c", "User \"#$<\"");
1041
1042     my $retryCount = 20;
1043     while (system("/usr/bin/curl -q --silent --stderr - --output /dev/null $listen") && $retryCount) {
1044         sleep 1;
1045         --$retryCount;
1046     }
1047     
1048     die "Timed out waiting for httpd to start" unless $retryCount;
1049     
1050     $isHttpdOpen = 1;
1051 }
1052
1053 sub closeHTTPD()
1054 {
1055     return if !$isHttpdOpen;
1056
1057     close HTTPDIN;
1058     close HTTPDOUT;
1059
1060     kill 15, `cat /tmp/WebKit/httpd.pid` if -f "/tmp/WebKit/httpd.pid";
1061
1062     $isHttpdOpen = 0;
1063 }
1064
1065 sub fileNameWithNumber($$)
1066 {
1067     my ($base, $number) = @_;
1068     return "$base$number" if ($number > 1);
1069     return $base;
1070 }
1071
1072 sub processIgnoreTests($) {
1073     my @ignoreList = split(/\s*,\s*/, shift);
1074     my $addIgnoredDirectories = sub {
1075         return () if exists $ignoredLocalDirectories{basename($File::Find::dir)};
1076         $ignoredDirectories{File::Spec->abs2rel($File::Find::dir, $testDirectory)} = 1;
1077         return @_;
1078     };
1079     foreach my $item (@ignoreList) {
1080         my $path = catfile($testDirectory, $item); 
1081         if (-d $path) {
1082             $ignoredDirectories{$item} = 1;
1083             find({ preprocess => $addIgnoredDirectories, wanted => sub {} }, $path);
1084         }
1085         elsif (-f $path) {
1086             $ignoredFiles{$item} = 1;
1087         }
1088         else {
1089             print "ignoring '$item' on ignore-tests list\n";
1090         }
1091     }
1092 }
1093
1094 sub isTextOnlyTest($)
1095 {
1096     my ($actual) = @_;
1097     my $isText;
1098     if ($actual !~ /^layer at/) {
1099         $isText = 1;
1100     } else {
1101         $isText = 0;
1102     }
1103     return $isText;
1104 }
1105
1106 sub expectedDirectoryForTest($)
1107 {
1108     my ($isText) = @_;
1109     if($isText) {
1110         return $expectedDirectoryText;
1111     } else {
1112         return $expectedDirectory;
1113     }
1114 }