LayoutTests:
[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         if ($test !~ /^http\/tests\/local\//) {
378             my $path = canonpath($test);
379             $path =~ s/^http\/tests\///;
380             print OUT "http://127.0.0.1:$httpdPort/$path\n";
381         }
382         else {
383             my $testPath = "$testDirectory/$test";
384             if (isCygwin()) {
385                 $testPath = `cygpath -m -s "$testPath"`;
386             }
387             else {
388                 $testPath = canonpath($testPath);
389             }
390             print OUT "$testPath\n";
391         }
392     }
393
394     my $actual = "";
395     while (<IN>) {
396         last if /#EOF/;
397         $actual .= $_;
398     }
399     my $isText = isTextOnlyTest($actual);
400
401     $durations{$test} = time - $startTime if $report10Slowest;
402
403     my $expected;
404     my $expectedDir = expectedDirectoryForTest($isText);
405     if (!$resetResults && open EXPECTED, "<", "$expectedDir/$base-$expectedTag.txt") {
406         $expected = "";
407         while (<EXPECTED>) {
408             $expected .= $_;
409         }
410         close EXPECTED;
411     }
412     my $expectedMac;
413     if (isQt() && $strictTesting && !$isText) {
414       if (!$resetResults && open EXPECTED, "<", "$expectedDirectoryText/$base-$expectedTag.txt") {
415         $expectedMac = "";
416         while (<EXPECTED>) {
417           $expectedMac .= $_;
418         }
419         close EXPECTED;
420       }
421     }
422
423     if ($shouldCheckLeaks && $testsPerDumpTool == 1) {
424         print "        $test -> ";
425     }
426
427     my $actualPNG = "";
428     my $diffPNG = "";
429     my $diffPercentage = "";
430     my $diffResult = "passed";
431     
432     if ($pixelTests) {
433         die "Pixel tests currently don't work when using a custom expected test results directory" if $testDirectory ne $expectedDir;
434             
435         my $actualHash = "";
436         my $expectedHash = "";
437         my $actualPNGSize = 0;
438         while (<IN>) {
439             last if /#EOF/;
440             if (/ActualHash: ([a-f0-9]{32})/) {
441                 $actualHash = $1;
442             } elsif (/BaselineHash: ([a-f0-9]{32})/) {
443                 $expectedHash = $1;
444             } elsif (/Content-length: (\d+)\s*/) {
445                 $actualPNGSize = $1;
446                 read(IN, $actualPNG, $actualPNGSize);
447             }
448         }
449
450         if ($expectedHash ne $actualHash && -f "$testDirectory/$base-$expectedTag.png") {
451             my $expectedPNGSize = -s "$testDirectory/$base-$expectedTag.png";
452             my $expectedPNG = "";
453             open EXPECTEDPNG, "$testDirectory/$base-$expectedTag.png";
454             read(EXPECTEDPNG, $expectedPNG, $expectedPNGSize);
455
456             print DIFFOUT "Content-length: $actualPNGSize\n";
457             print DIFFOUT $actualPNG;
458
459             print DIFFOUT "Content-length: $expectedPNGSize\n";
460             print DIFFOUT $expectedPNG;
461
462             while (<DIFFIN>) {
463                 last if /^error/ || /^diff:/;
464                 if (/Content-length: (\d+)\s*/) {
465                     read(DIFFIN, $diffPNG, $1);
466                 }
467             }
468
469             if (/^diff: (.+)% (passed|failed)/) {
470                 $diffPercentage = $1;
471                 $diffResult = $2;
472             }
473         }
474
475         if ($actualPNGSize && ($resetResults || !-f "$testDirectory/$base-$expectedTag.png")) {
476             open EXPECTED, ">", "$testDirectory/$base-expected.png" or die "could not create $testDirectory/$base-expected.png\n";
477             print EXPECTED $actualPNG;
478             close EXPECTED;
479         }
480
481         # update the expected hash if the image diff said that there was no difference
482         if ($actualHash ne "" && ($resetResults || !-f "$testDirectory/$base-$expectedTag.checksum")) {
483             open EXPECTED, ">", "$testDirectory/$base-$expectedTag.checksum" or die "could not create $testDirectory/$base-$expectedTag.checksum\n";
484             print EXPECTED $actualHash;
485             close EXPECTED;
486         }
487     }
488
489     if (isQt() && $strictTesting && !$isText) {
490       if (defined $expectedMac) {
491         my $simplified_actual;
492         $simplified_actual = $actual;
493         $simplified_actual =~ s/at \(-?[0-9]+,-?[0-9]+\) *//g;
494         $simplified_actual =~ s/size -?[0-9]+x-?[0-9]+ *//g;
495         $simplified_actual =~ s/text run width -?[0-9]+: //g;
496         $simplified_actual =~ s/\([0-9]+px/px/g;
497         $simplified_actual =~ s/ *" *\n +" */ /g;
498         $simplified_actual =~ s/" +$/"/g;
499
500         $simplified_actual =~ s/- /-/g;
501         $simplified_actual =~ s/\n( *)"\s+/\n$1"/g;
502         $simplified_actual =~ s/\s+"\n/"\n/g;
503         
504         $expectedMac =~ s/at \(-?[0-9]+,-?[0-9]+\) *//g;
505         $expectedMac =~ s/size -?[0-9]+x-?[0-9]+ *//g;
506         $expectedMac =~ s/text run width -?[0-9]+: //g;
507         $expectedMac =~ s/\([0-9]+px/px/g;
508         $expectedMac =~ s/ *" *\n +" */ /g;
509         $expectedMac =~ s/" +$/"/g;
510
511         $expectedMac =~ s/- /-/g;
512         $expectedMac =~ s/\n( *)"\s+/\n$1"/g;
513         $expectedMac =~ s/\s+"\n/"\n/g;
514         
515         if ($simplified_actual ne $expectedMac) {
516           open ACTUAL, ">", "/tmp/actual.txt" or die;
517           print ACTUAL $simplified_actual;
518           close ACTUAL;
519           open ACTUAL, ">", "/tmp/expected.txt" or die;
520           print ACTUAL $expectedMac;
521           close ACTUAL;
522           system "diff -u \"/tmp/expected.txt\" \"/tmp/actual.txt\" > \"/tmp/simplified.diff\"";
523
524           $diffResult = "failed";
525           if($verbose) {
526             print "\n";
527             system "cat /tmp/simplified.diff";
528             print "failed!!!!!";
529           }
530         }
531       }
532     }
533
534     if (!defined $expected) {
535         if ($verbose) {
536             print "new " . ($resetResults ? "result" : "test") ."\n";
537             $atLineStart = 1;
538         }
539         $result = "new";
540
541         if ($generateNewResults || $resetResults) {
542             # Create the path if needed
543             mkpath(catfile($expectedDir, dirname($base))) if $testDirectory ne $expectedDir;
544
545             open EXPECTED, ">", "$expectedDir/$base-$expectedTag.txt" or die "could not create $expectedDir/$base-$expectedTag.txt\n";
546             print EXPECTED $actual;
547             close EXPECTED;
548         }
549         unlink "$testResultsDirectory/$base-$actualTag.txt";
550         unlink "$testResultsDirectory/$base-$diffsTag.txt";
551         unless ($resetResults) {
552             # Always print the file name for new tests, as they will probably need some manual inspection.
553             # in verbose mode we already printed the test case, so no need to do it again.
554             unless ($verbose) {
555                 print "\n" unless $atLineStart;
556                 print "$test -> ";
557             }
558             print "new\n";
559         }
560         $atLineStart = 1;
561     } elsif ($actual eq $expected && $diffResult eq "passed") {
562         if ($verbose) {
563             print "succeeded\n";
564             $atLineStart = 1;
565         }
566         $result = "match";
567         unlink "$testResultsDirectory/$base-$actualTag.txt";
568         unlink "$testResultsDirectory/$base-$diffsTag.txt";
569     } else {
570         unless ($verbose) {
571             print "\n" unless $atLineStart;
572             print "$test -> ";
573         }
574         print "failed\n";
575         $atLineStart = 1;
576
577         $result = "mismatch";
578
579         my $dir = "$testResultsDirectory/$base";
580         $dir =~ s|/([^/]+)$|| or die "Failed to find test name from base\n";
581         my $testName = $1;
582         mkpath $dir;
583
584         open ACTUAL, ">", "$testResultsDirectory/$base-$actualTag.txt" or die;
585         print ACTUAL $actual;
586         close ACTUAL;
587
588         system "diff -u \"$expectedDir/$base-$expectedTag.txt\" \"$testResultsDirectory/$base-$actualTag.txt\" > \"$testResultsDirectory/$base-$diffsTag.txt\"";
589
590         if ($pixelTests && $diffPNG && $diffPNG ne "") {
591             $imagesPresent{$base} = 1;
592
593             open ACTUAL, ">", "$testResultsDirectory/$base-$actualTag.png" or die;
594             print ACTUAL $actualPNG;
595             close ACTUAL;
596
597             open DIFF, ">", "$testResultsDirectory/$base-$diffsTag.png" or die;
598             print DIFF $diffPNG;
599             close DIFF;
600             
601             copy("$expectedDir/$base-$expectedTag.png", "$testResultsDirectory/$base-$expectedTag.png");
602
603             open DIFFHTML, ">$testResultsDirectory/$base-$diffsTag.html" or die;
604             print DIFFHTML "<html>\n";
605             print DIFFHTML "<head>\n";
606             print DIFFHTML "<title>$base Image Compare</title>\n";
607             print DIFFHTML "<script language=\"Javascript\" type=\"text/javascript\">\n";
608             print DIFFHTML "var currentImage = 0;\n";
609             print DIFFHTML "var imageNames = new Array(\"Actual\", \"Expected\");\n";
610             print DIFFHTML "var imagePaths = new Array(\"$testName-$actualTag.png\", \"$testName-$expectedTag.png\");\n";
611             if (-f "$testDirectory/$base-w3c.png") {
612                 copy("$testDirectory/$base-w3c.png", "$testResultsDirectory/$base-w3c.png");
613                 print DIFFHTML "imageNames.push(\"W3C\");\n";
614                 print DIFFHTML "imagePaths.push(\"$testName-w3c.png\");\n";
615             }
616             print DIFFHTML "function animateImage() {\n";
617             print DIFFHTML "    var image = document.getElementById(\"animatedImage\");\n";
618             print DIFFHTML "    var imageText = document.getElementById(\"imageText\");\n";
619             print DIFFHTML "    image.src = imagePaths[currentImage];\n";
620             print DIFFHTML "    imageText.innerHTML = imageNames[currentImage] + \" Image\";\n";
621             print DIFFHTML "    currentImage = (currentImage + 1) % imageNames.length;\n";
622             print DIFFHTML "    setTimeout('animateImage()',2000);\n";
623             print DIFFHTML "}\n";
624             print DIFFHTML "</script>\n";
625             print DIFFHTML "</head>\n";
626             print DIFFHTML "<body onLoad=\"animateImage();\">\n";
627             print DIFFHTML "<table>\n";
628             if ($diffPercentage) {
629                 print DIFFHTML "<tr>\n";
630                 print DIFFHTML "<td>Difference between images: <a href=\"$testName-$diffsTag.png\">$diffPercentage%</a></td>\n";
631                 print DIFFHTML "</tr>\n";
632             }
633             print DIFFHTML "<tr>\n";
634             print DIFFHTML "<td id=\"imageText\" style=\"text-weight: bold;\">Actual Image</td>\n";
635             print DIFFHTML "</tr>\n";
636             print DIFFHTML "<tr>\n";
637             print DIFFHTML "<td><img src=\"$testName-$actualTag.png\" id=\"animatedImage\"></td>\n";
638             print DIFFHTML "</tr>\n";
639             print DIFFHTML "</table>\n";
640             print DIFFHTML "</body>\n";
641             print DIFFHTML "</html>\n";
642         }
643     }
644
645     if (($count + 1) % $testsPerDumpTool == 0 || $count == $#tests) {
646         if ($shouldCheckLeaks) {
647             my $fileName;
648             if ($testsPerDumpTool == 1) {
649                 $fileName = "$testResultsDirectory/$base-leaks.txt";
650             } else {
651                 $fileName = "$testResultsDirectory/" . fileNameWithNumber($dumpToolName, $leaksOutputFileNumber) . "-leaks.txt";
652             }
653             my $leakCount = countAndPrintLeaks($dumpToolName, $dumpToolPID, $fileName);
654             $totalLeaks += $leakCount;
655             $leaksOutputFileNumber++ if ($leakCount);
656         }
657
658         closeDumpTool();
659     }
660
661     $count++;
662     $counts{$result}++;
663     push @{$tests{$result}}, $test;
664     $testType{$test} = $isText;
665 }
666 printf "\n%0.2fs total testing time\n", (time - $overallStartTime) . "";
667
668 !$isDumpToolOpen || die "Failed to close $dumpToolName.\n";
669
670 closeHTTPD();
671
672 system "rm /tmp/LayoutTests";
673
674 # FIXME: Do we really want to check the image-comparison tool for leaks every time?
675 if ($shouldCheckLeaks && $pixelTests) {
676     $totalLeaks += countAndPrintLeaks("ImageDiff", $imageDiffToolPID, "$testResultsDirectory/ImageDiff-leaks.txt");
677 }
678
679 if ($totalLeaks) {
680     print "\nWARNING: $totalLeaks total leaks found!\n";
681     print "See above for individual leaks results.\n" if ($leaksOutputFileNumber > 2);
682 }
683
684 close IN;
685 close OUT;
686
687 if ($report10Slowest) {
688     print "\n\nThe 10 slowest tests:\n\n";
689     my $count = 0;
690     for my $test (sort slowestcmp keys %durations) {
691         printf "%0.2f secs: %s\n", $durations{$test}, $test;
692         last if ++$count == 10;
693     }
694 }
695
696 print "\n";
697
698 if ($resetResults || ($counts{match} && $counts{match} == $count)) {
699     print "all $count test cases succeeded\n";
700     unlink $testResults;
701     exit;
702 }
703
704
705 my %text = (
706     match => "succeeded",
707     mismatch => "had incorrect layout",
708     new => "were new",
709     fail => "failed (tool did not execute successfully)",
710 );
711
712 for my $type ("match", "mismatch", "new", "fail") {
713     my $c = $counts{$type};
714     if ($c) {
715         my $t = $text{$type};
716         my $message;
717         if ($c == 1) {
718             $t =~ s/were/was/;
719             $message = sprintf "1 test case (%d%%) %s\n", 1 * 100 / $count, $t;
720         } else {
721             $message = sprintf "%d test cases (%d%%) %s\n", $c, $c * 100 / $count, $t;
722         }
723         $message =~ s-\(0%\)-(<1%)-;
724         print $message;
725     }
726 }
727
728 mkpath $testResultsDirectory;
729
730 open HTML, ">", $testResults or die;
731 print HTML "<html>\n";
732 print HTML "<head>\n";
733 print HTML "<title>Layout Test Results</title>\n";
734 print HTML "</head>\n";
735 print HTML "<body>\n";
736
737 if ($counts{mismatch}) {
738     print HTML "<p>Tests where results did not match expected results:</p>\n";
739     print HTML "<table>\n";
740     for my $test (@{$tests{mismatch}}) {
741         my $base = $test;
742         $base =~ s/\.[a-zA-Z]+$//;
743         my $expectedDir;
744         if ($testType{$test}) {
745             $expectedDir = $expectedDirectoryText;
746         } else {
747             $expectedDir = $expectedDirectory;
748         }
749         copy("$expectedDir/$base-$expectedTag.txt", "$testResultsDirectory/$base-$expectedTag.txt");
750         print HTML "<tr>\n";            
751         print HTML "<td><a href=\"$testDirectory/$test\">$base</a></td>\n";
752         if (-s "$testResultsDirectory/$base-$diffsTag.txt") {
753             print HTML "<td><a href=\"$base-$expectedTag.txt\">expected</a></td>\n";
754             print HTML "<td><a href=\"$base-$actualTag.txt\">actual</a></td>\n";
755             print HTML "<td><a href=\"$base-$diffsTag.txt\">diffs</a></td>\n";
756         } else {
757             print HTML "<td></td><td></td><td></td>\n";
758         }
759         if ($pixelTests) {
760             if ($imagesPresent{$base}) {
761                 print HTML "<td><a href=\"$base-$expectedTag.png\">expected image</a></td>\n";
762                 print HTML "<td><a href=\"$base-$diffsTag.html\">image diffs</a></td>\n";
763             } else {
764                 print HTML "<td></td><td></td>\n";
765             }            
766         }
767         print HTML "</tr>\n";
768     }
769     print HTML "</table>\n";
770 }
771
772 if ($counts{fail}) {
773     print HTML "<p>Tests that caused the DumpRenderTree tool to fail:</p>\n";
774     print HTML "<table>\n";
775     for my $test (@{$tests{fail}}) {
776         my $base = $test;
777         my $expectedDir;
778         if ($testType{$test}) {
779             $expectedDir = $expectedDirectoryText;
780         } else {
781             $expectedDir = $expectedDirectory;
782         }
783         $base =~ s/\.[a-zA-Z]+$//;
784         print HTML "<tr>\n";
785         print HTML "<td><a href=\"$testDirectory/$test\">$base</a></td>\n";
786         print HTML "</tr>\n";
787     }
788     print HTML "</table>\n";
789 }
790
791 if ($counts{new}) {
792     print HTML "<p>Tests that had no expected results (probably new):</p>\n";
793     print HTML "<table>\n";
794     for my $test (@{$tests{new}}) {
795         my $base = $test;
796         my $expectedDir;
797         if ($testType{$test}) {
798             $expectedDir = $expectedDirectoryText;
799         } else {
800             $expectedDir = $expectedDirectory;
801         }
802         $base =~ s/\.[a-zA-Z]+$//;
803         print HTML "<tr>\n";
804         print HTML "<td><a href=\"$expectedDir/$test\">$base</a></td>\n";
805         print HTML "<td><a href=\"$expectedDir/$base-$expectedTag.txt\">results</a></td>\n";
806         if ($pixelTests && -f "$expectedDir/$base-$expectedTag.png") {
807             print HTML "<td><a href=\"$expectedDir/$base-$expectedTag.png\">image</a></td>\n";
808         }
809         print HTML "</tr>\n";
810     }
811     print HTML "</table>\n";
812 }
813
814 print HTML "</body>\n";
815 print HTML "</html>\n";
816 close HTML;
817
818 if(isQt()) {
819   system "konqueror", $testResults if $launchSafari;
820 } else {
821   system "WebKitTools/Scripts/run-safari", $configurationOption, "-NSOpen", $testResults if $launchSafari;
822 }
823
824 exit 1;
825
826 sub countAndPrintLeaks($$$)
827 {
828     my ($dumpToolName, $dumpToolPID, $leaksFilePath) = @_;
829
830     print "\n" unless $atLineStart;
831     $atLineStart = 1;
832
833     # We are excluding the following reported leaks so they don't get in our way when looking for WebKit leaks:
834
835     my @typesToExclude = (
836         "THRD", # bug in 'leaks', Radar 3387783
837         "DRHT" #ditto (endian little hate i)
838     );
839
840     my @callStacksToExclude = (
841         "_CFPreferencesDomainDeepCopyDictionary", # leak apparently in CFPreferences, Radar 4220786
842         "FOGetCoveredUnicodeChars", # leak apparently in ATS, Radar 3943604
843         "PCFragPrepareClosureFromFile", # leak in Code Fragment Manager, Radar 3426998
844         "Flash_EnforceLocalSecurity", # leaks in flash plugin code, Radar 4449747
845         "ICCFPrefWrapper::GetPrefDictionary", # leaks in quicktime plugin code, Radar 4449794
846         "GetLineDirectionPreference", "InitUnicodeUtilities", # leak apparently in CFNotificationCenterAddObserver, Radar 4964790
847         "CFRunLoopRunSpecific \\| malloc_zone_malloc", # leak in CFRunLoopRunSpecific, Radar 4670839
848         "CFRunLoopRunSpecific \\| CFAllocatorAllocate ", # ditto
849         "_objc_msgForward", # leak apparently in NSSpellChecker, Radar 4965278
850         "WebCore::Selection::toRange", # bug in 'leaks', Radar 4967949
851         "WebCore::SubresourceLoader::create", # bug in 'leaks', Radar 4985806
852         "NSURLCache cachedResponseForRequest", # leak in CFURL cache, Radar 4768430
853         "NSAttributedString\\(NSAttributedStringKitAdditions\\) _initWithDOMRange:", # leak apparently in NSAttributedString, Radar 4970608
854         "NSAttributedString\\(NSAttributedStringKitAdditions\\) dataFromRange:documentAttributes:error:" # leak apparently in NSAttributedString, Radar 4973752
855     );
856
857     # Note that the exclusion for bug 3387783 doesn't quite work right; sometimes a leak of 'THRD' will
858     # still appear in the leaks output.
859
860     my $leaksTool = sourceDir() . "/WebKitTools/Scripts/run-leaks";
861     my $excludeString = "--exclude-callstack '" . (join "' --exclude-callstack '", @callStacksToExclude) . "' " .
862                         "--exclude-type '" . (join "' --exclude-type '", @typesToExclude) . "'";
863
864     print " ? checking for leaks in $dumpToolName\n";
865     my $leaksOutput = `$leaksTool $excludeString $dumpToolPID`;
866     my ($count, $bytes) = $leaksOutput =~ /Process $dumpToolPID: (\d+) leaks? for (\d+) total/;
867     my ($excluded) = $leaksOutput =~ /(\d+) leaks? excluded/;
868
869     my $adjustedCount = $count;
870     $adjustedCount -= $excluded if $excluded;
871
872     if (!$adjustedCount) {
873         print " - no leaks found\n";
874         unlink $leaksFilePath;
875         return 0;
876     } else {
877         my $dir = $leaksFilePath;
878         $dir =~ s|/[^/]+$|| or die;
879         mkpath $dir;
880
881         if ($excluded) {
882             print " + $adjustedCount leaks ($bytes bytes including $excluded excluded leaks) were found, details in $leaksFilePath\n";
883         } else {
884             print " + $count leaks ($bytes bytes) were found, details in $leaksFilePath\n";
885         }
886
887         open LEAKS, ">", $leaksFilePath or die;
888         print LEAKS $leaksOutput;
889         close LEAKS;
890     }
891
892     return $adjustedCount;
893 }
894
895 # Break up a path into the directory (with slash) and base name.
896 sub splitpath($)
897 {
898     my ($path) = @_;
899
900     my $pathSeparator = "/";
901     my $dirname = dirname($path) . $pathSeparator;
902     $dirname = "" if $dirname eq "." . $pathSeparator;
903
904     return ($dirname, basename($path));
905 }
906
907 # Sort first by directory, then by file, so all paths in one directory are grouped
908 # rather than being interspersed with items from subdirectories.
909 # Use numericcmp to sort directory and filenames to make order logical.
910 sub pathcmp($$)
911 {
912     my ($patha, $pathb) = @_;
913
914     my ($dira, $namea) = splitpath($patha);
915     my ($dirb, $nameb) = splitpath($pathb);
916
917     return numericcmp($dira, $dirb) if $dira ne $dirb;
918     return numericcmp($namea, $nameb);
919 }
920
921 # Sort numeric parts of strings as numbers, other parts as strings.
922 # Makes 1.33 come after 1.3, which is cool.
923 sub numericcmp($$)
924 {
925     my ($aa, $bb) = @_;
926
927     my @a = split /(\d+)/, $aa;
928     my @b = split /(\d+)/, $bb;
929
930     # Compare one chunk at a time.
931     # Each chunk is either all numeric digits, or all not numeric digits.
932     while (@a && @b) {
933         my $a = shift @a;
934         my $b = shift @b;
935         
936         # Use numeric comparison if chunks are non-equal numbers.
937         return $a <=> $b if $a =~ /^\d/ && $b =~ /^\d/ && $a != $b;
938
939         # Use string comparison if chunks are any other kind of non-equal string.
940         return $a cmp $b if $a ne $b;
941     }
942     
943     # One of the two is now empty; compare lengths for result in this case.
944     return @a <=> @b;
945 }
946
947 # Sort slowest tests first.
948 sub slowestcmp($$)
949 {
950     my ($testa, $testb) = @_;
951
952     my $dura = $durations{$testa};
953     my $durb = $durations{$testb};
954     return $durb <=> $dura if $dura != $durb;
955     return pathcmp($testa, $testb);
956 }
957
958 sub openDumpTool()
959 {
960     return if $isDumpToolOpen;
961
962     # Save some requires variables for the linux environment...
963     my $homeDir = $ENV{'HOME'};
964     my $libraryPath = $ENV{'LD_LIBRARY_PATH'};
965     my $dbusAddress = $ENV{'DBUS_SESSION_BUS_ADDRESS'};
966     my $display = $ENV{'DISPLAY'};
967
968     my $isQt = isQt();
969     local %ENV;
970     if ($isQt) {
971         if (defined $display) {
972             $ENV{DISPLAY} = $display;
973         } else {
974             $ENV{DISPLAY} = ":1";
975         }
976         $ENV{HOME} = $homeDir;
977         if (defined $libraryPath) {
978             $ENV{LD_LIBRARY_PATH} = $libraryPath;
979         }
980         if (defined $dbusAddress) {
981             $ENV{DBUS_SESSION_BUS_ADDRESS} = $dbusAddress;
982         }
983     }
984     $ENV{DYLD_FRAMEWORK_PATH} = $productDir;
985     $ENV{XML_CATALOG_FILES} = ""; # work around missing /etc/catalog <rdar://problem/4292995>
986     $ENV{MallocStackLogging} = 1 if $shouldCheckLeaks;
987     $ENV{DYLD_INSERT_LIBRARIES} = "/usr/lib/libgmalloc.dylib" if $guardMalloc;
988     my @args = ();
989     if ($useValgrind) {
990       push @args, $dumpTool;
991     }
992     push @args, @toolArgs;
993     if ($useValgrind) {
994       $dumpTool = "valgrind";
995     }
996     $dumpToolPID = open2(\*IN, \*OUT, $dumpTool, @args) or die "Failed to start tool: $dumpTool\n";
997     $isDumpToolOpen = 1;
998 }
999
1000 sub closeDumpTool()
1001 {
1002     return if !$isDumpToolOpen;
1003
1004     close IN;
1005     close OUT;
1006     waitpid $dumpToolPID, 0;
1007     $isDumpToolOpen = 0;
1008 }
1009
1010 sub openHTTPDIfNeeded()
1011 {
1012     return if $isHttpdOpen;
1013
1014     mkdir "/tmp/WebKit";
1015     
1016     if (-f "/tmp/WebKit/httpd.pid") {
1017         my $oldPid = `cat /tmp/WebKit/httpd.pid`;
1018         chomp $oldPid;
1019         if (0 != kill 0, $oldPid) {
1020             print "\nhttpd is already running: pid $oldPid, killing...\n";
1021             kill 15, $oldPid;
1022             
1023             my $retryCount = 20;
1024             while ((0 != kill 0, $oldPid) && $retryCount) {
1025                 sleep 1;
1026                 --$retryCount;
1027             }
1028             
1029             die "Timed out waiting for httpd to quit" unless $retryCount;
1030         }
1031     }
1032     
1033     my $httpdPath = "/usr/sbin/httpd";
1034     my $httpdConfig = "$testDirectory/http/conf/httpd.conf";
1035     $httpdConfig = "$testDirectory/http/conf/apache2-httpd.conf" if `$httpdPath -v` =~ m|Apache/2|;
1036     my $documentRoot = "$testDirectory/http/tests";
1037     my $typesConfig = "$testDirectory/http/conf/mime.types";
1038     my $listen = "127.0.0.1:$httpdPort";
1039     my $absTestResultsDirectory = File::Spec->rel2abs(glob $testResultsDirectory);
1040
1041     mkpath $absTestResultsDirectory;
1042
1043     open2(\*HTTPDIN, \*HTTPDOUT, $httpdPath, 
1044         "-f", "$httpdConfig",
1045         "-C", "DocumentRoot \"$documentRoot\"",
1046         "-C", "Listen $listen",
1047         "-c", "TypesConfig \"$typesConfig\"",
1048         "-c", "CustomLog \"$absTestResultsDirectory/access_log.txt\" common",
1049         "-c", "ErrorLog \"$absTestResultsDirectory/error_log.txt\"",
1050         # Apache wouldn't run CGIs with permissions==700 otherwise
1051         "-c", "User \"#$<\"");
1052
1053     my $retryCount = 20;
1054     while (system("/usr/bin/curl -q --silent --stderr - --output /dev/null $listen") && $retryCount) {
1055         sleep 1;
1056         --$retryCount;
1057     }
1058     
1059     die "Timed out waiting for httpd to start" unless $retryCount;
1060     
1061     $isHttpdOpen = 1;
1062 }
1063
1064 sub closeHTTPD()
1065 {
1066     return if !$isHttpdOpen;
1067
1068     close HTTPDIN;
1069     close HTTPDOUT;
1070
1071     kill 15, `cat /tmp/WebKit/httpd.pid` if -f "/tmp/WebKit/httpd.pid";
1072
1073     $isHttpdOpen = 0;
1074 }
1075
1076 sub fileNameWithNumber($$)
1077 {
1078     my ($base, $number) = @_;
1079     return "$base$number" if ($number > 1);
1080     return $base;
1081 }
1082
1083 sub processIgnoreTests($) {
1084     my @ignoreList = split(/\s*,\s*/, shift);
1085     my $addIgnoredDirectories = sub {
1086         return () if exists $ignoredLocalDirectories{basename($File::Find::dir)};
1087         $ignoredDirectories{File::Spec->abs2rel($File::Find::dir, $testDirectory)} = 1;
1088         return @_;
1089     };
1090     foreach my $item (@ignoreList) {
1091         my $path = catfile($testDirectory, $item); 
1092         if (-d $path) {
1093             $ignoredDirectories{$item} = 1;
1094             find({ preprocess => $addIgnoredDirectories, wanted => sub {} }, $path);
1095         }
1096         elsif (-f $path) {
1097             $ignoredFiles{$item} = 1;
1098         }
1099         else {
1100             print "ignoring '$item' on ignore-tests list\n";
1101         }
1102     }
1103 }
1104
1105 sub isTextOnlyTest($)
1106 {
1107     my ($actual) = @_;
1108     my $isText;
1109     if ($actual !~ /^layer at/) {
1110         $isText = 1;
1111     } else {
1112         $isText = 0;
1113     }
1114     return $isText;
1115 }
1116
1117 sub expectedDirectoryForTest($)
1118 {
1119     my ($isText) = @_;
1120     if($isText) {
1121         return $expectedDirectoryText;
1122     } else {
1123         return $expectedDirectory;
1124     }
1125 }