Update safariVersion to safariVersionString
[WebKit-https.git] / Tools / Scripts / bisect-builds
1 #!/usr/bin/perl -w
2
3 # Copyright (C) 2007-2009, 2011-2014 Apple Inc.  All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions
7 # are met:
8 #
9 # 1.  Redistributions of source code must retain the above copyright
10 #     notice, this list of conditions and the following disclaimer. 
11 # 2.  Redistributions in binary form must reproduce the above copyright
12 #     notice, this list of conditions and the following disclaimer in the
13 #     documentation and/or other materials provided with the distribution. 
14 # 3.  Neither the name of Apple Inc. ("Apple") nor the names of
15 #     its contributors may be used to endorse or promote products derived
16 #     from this software without specific prior written permission. 
17 #
18 # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 # This script attempts to find the point at which a regression (or progression)
30 # of behavior occurred by searching WebKit nightly builds.
31
32 # To override the location where the nightly builds are downloaded or the path
33 # to the Safari web browser, create a ~/.bisect-buildsrc file with one or more of
34 # the following lines (use "~/" to specify a path from your home directory):
35 #
36 # $branch = "branch-name";
37 # $nightlyDownloadDirectory = "~/path/to/nightly/downloads";
38 # $safariPath = "/path/to/Safari.app";
39
40 use strict;
41
42 use File::Basename;
43 use File::Path;
44 use File::Spec;
45 use File::Temp qw(tempfile);
46 use FindBin;
47 use Getopt::Long;
48 use Time::HiRes qw(usleep);
49
50 use lib $FindBin::Bin;
51 use webkitdirs qw(safariPathFromSafariBundle);
52
53 sub createTempFile($);
54 sub downloadNightly($$$);
55 sub findMacOSXVersion();
56 sub findNearestNightlyIndex(\@$$);
57 sub findSafariVersion($);
58 sub loadSettings();
59 sub makeNightlyList($$$$);
60 sub max($$) { return $_[0] > $_[1] ? $_[0] : $_[1]; }
61 sub mountAndRunNightly($$$$);
62 sub parseRevisions($$;$);
63 sub printStatus($$$);
64 sub printTracLink($$);
65 sub promptForTest($);
66
67 loadSettings();
68
69 my %validBranches = map { $_ => 1 } qw(feature-branch trunk);
70 my $branch = $Settings::branch;
71 my $nightlyDownloadDirectory = $Settings::nightlyDownloadDirectory;
72 my $safariPath = $Settings::safariPath;
73
74 my @nightlies;
75
76 my $isProgression;
77 my $localOnly;
78 my @revisions;
79 my $sanityCheck;
80 my $showHelp;
81 my $testURL;
82
83 # Fix up -r switches in @ARGV
84 @ARGV = map { /^(-r)(.+)$/ ? ($1, $2) : $_ } @ARGV;
85
86 my $result = GetOptions(
87     sharedCommandLineOptions(),
88     "b|branch=s"             => \$branch,
89     "d|download-directory=s" => \$nightlyDownloadDirectory,
90     "h|help"                 => \$showHelp,
91     "l|local!"               => \$localOnly,
92     "p|progression!"         => \$isProgression,
93     "r|revisions=s"          => \&parseRevisions,
94     "safari-path=s"          => \$safariPath,
95     "s|sanity-check!"        => \$sanityCheck,
96 );
97 $testURL = shift @ARGV;
98
99 $branch = "feature-branch" if $branch eq "feature";
100 if (!exists $validBranches{$branch}) {
101     print STDERR "ERROR: Invalid branch '$branch'\n";
102     $showHelp = 1;
103 }
104
105 if (!$result || $showHelp || scalar(@ARGV) > 0) {
106     print STDERR "Search WebKit nightly builds for changes in behavior.\n";
107     print STDERR "Usage: " . basename($0) . " [options] [url]\n";
108     print STDERR <<END;
109   [-b|--branch name]             name of the nightly build branch (default: trunk)
110   [-d|--download-directory dir]  nightly build download directory (default: ~/Library/Caches/WebKit-Nightlies)
111   [-h|--help]                    show this help message
112   [-l|--local]                   only use local (already downloaded) nightlies
113   [-p|--progression]             searching for a progression, not a regression
114   [-r|--revision M[:N]]          specify starting (and optional ending) revisions to search
115   [--safari-path path]           path to Safari application bundle (default: /Applications/Safari.app)
116   [-s|--sanity-check]            verify both starting and ending revisions before bisecting
117 END
118     print STDERR sharedCommandLineOptionsUsage(brackets => 1, indent => 2, switchWidth => 30);
119     exit 1;
120 }
121
122 my $nightlyWebSite = "http://nightly.webkit.org";
123 my $nightlyBuildsURLBase = $nightlyWebSite . File::Spec->catdir("/builds", $branch, "mac");
124 my $nightlyFilesURLBase = $nightlyWebSite . File::Spec->catdir("/files", $branch, "mac");
125
126 $nightlyDownloadDirectory = glob($nightlyDownloadDirectory) if $nightlyDownloadDirectory =~ /^~/;
127 $safariPath = glob($safariPath) if $safariPath =~ /^~/;
128 $safariPath = safariPathFromSafariBundle($safariPath) if $safariPath =~ m#\.app/*#;
129
130 $nightlyDownloadDirectory = File::Spec->catdir($nightlyDownloadDirectory, $branch);
131 if (! -d $nightlyDownloadDirectory) {
132     mkpath($nightlyDownloadDirectory, 0, 0755) || die "Could not create $nightlyDownloadDirectory: $!";
133 }
134
135 @nightlies = makeNightlyList($localOnly, $nightlyDownloadDirectory, findMacOSXVersion(), findSafariVersion($safariPath));
136
137 my $startIndex = $revisions[0] ? findNearestNightlyIndex(@nightlies, $revisions[0], 'ceil') : 0;
138 my $endIndex = $revisions[1] ? findNearestNightlyIndex(@nightlies, $revisions[1], 'floor') : $#nightlies;
139
140 my $tempFile = createTempFile($testURL);
141
142 if ($sanityCheck) {
143     my $didReproduceBug;
144
145     do {
146         printf "\nChecking starting revision r%s...\n",
147             $nightlies[$startIndex]->{rev};
148         downloadNightly($nightlies[$startIndex]->{file}, $nightlyFilesURLBase, $nightlyDownloadDirectory);
149         mountAndRunNightly($nightlies[$startIndex]->{file}, $nightlyDownloadDirectory, $safariPath, $tempFile);
150         $didReproduceBug = promptForTest($nightlies[$startIndex]->{rev});
151         $startIndex-- if $didReproduceBug < 0;
152     } while ($didReproduceBug < 0);
153     die "ERROR: Bug reproduced in starting revision!  Do you need to test an earlier revision or for a progression?"
154         if $didReproduceBug && !$isProgression;
155     die "ERROR: Bug not reproduced in starting revision!  Do you need to test an earlier revision or for a regression?"
156         if !$didReproduceBug && $isProgression;
157
158     do {
159         printf "\nChecking ending revision r%s...\n",
160             $nightlies[$endIndex]->{rev};
161         downloadNightly($nightlies[$endIndex]->{file}, $nightlyFilesURLBase, $nightlyDownloadDirectory);
162         mountAndRunNightly($nightlies[$endIndex]->{file}, $nightlyDownloadDirectory, $safariPath, $tempFile);
163         $didReproduceBug = promptForTest($nightlies[$endIndex]->{rev});
164         $endIndex++ if $didReproduceBug < 0;
165     } while ($didReproduceBug < 0);
166     die "ERROR: Bug NOT reproduced in ending revision!  Do you need to test a later revision or for a progression?"
167         if !$didReproduceBug && !$isProgression;
168     die "ERROR: Bug reproduced in ending revision!  Do you need to test a later revision or for a regression?"
169         if $didReproduceBug && $isProgression;
170 }
171
172 printStatus($nightlies[$startIndex]->{rev}, $nightlies[$endIndex]->{rev}, $isProgression);
173
174 my %brokenRevisions = ();
175 while (abs($endIndex - $startIndex) > 1) {
176     my $index = $startIndex + int(($endIndex - $startIndex) / 2);
177
178     my $didReproduceBug;
179     do {
180         if (exists $nightlies[$index]) {
181             my $buildsLeft = max(max(0, $endIndex - $index - 1), max(0, $index - $startIndex - 1));
182             my $plural = $buildsLeft == 1 ? "" : "s";
183             printf "\nChecking revision r%s (%d build%s left to test after this)...\n", $nightlies[$index]->{rev}, $buildsLeft, $plural;
184             downloadNightly($nightlies[$index]->{file}, $nightlyFilesURLBase, $nightlyDownloadDirectory);
185             mountAndRunNightly($nightlies[$index]->{file}, $nightlyDownloadDirectory, $safariPath, $tempFile);
186             $didReproduceBug = promptForTest($nightlies[$index]->{rev});
187         }
188         if ($didReproduceBug < 0) {
189             $brokenRevisions{$nightlies[$index]->{rev}} = $nightlies[$index]->{file};
190             delete $nightlies[$index];
191             $endIndex--;
192             if (scalar(keys %brokenRevisions) % 2 == 0) {
193                 # Even tries to bisect to the left
194                 $index = int(($startIndex + $index) / 2);
195             } else {
196                 # Odd tries to bisect to the right
197                 $index = int(($index + $endIndex) / 2);
198             }
199         }
200     } while ($didReproduceBug < 0);
201
202     if ($didReproduceBug && !$isProgression || !$didReproduceBug && $isProgression) {
203         $endIndex = $index;
204     } else {
205         $startIndex = $index;
206     }
207
208     print "\nBroken revisions skipped: r" . join(", r", keys %brokenRevisions) . "\n"
209         if scalar keys %brokenRevisions > 0;
210     printStatus($nightlies[$startIndex]->{rev}, $nightlies[$endIndex]->{rev}, $isProgression);
211 }
212
213 printTracLink($nightlies[$startIndex]->{rev}, $nightlies[$endIndex]->{rev});
214
215 unlink $tempFile if $tempFile;
216
217 exit 0;
218
219 sub createTempFile($)
220 {
221     my ($url) = @_;
222
223     return undef if !$url;
224
225     my ($fh, $tempFile) = tempfile(
226         basename($0) . "-XXXXXXXX",
227         DIR => File::Spec->tmpdir(),
228         SUFFIX => ".html",
229         UNLINK => 0,
230     );
231     print $fh "<meta http-equiv=\"refresh\" content=\"0; $url\">\n";
232     close($fh);
233
234     return $tempFile;
235 }
236
237 sub downloadNightly($$$)
238 {
239     my ($filename, $urlBase, $directory) = @_;
240     my $path = File::Spec->catfile($directory, $filename);
241     if (! -f $path) {
242         print "Downloading $filename to $directory...\n";
243         `curl -# -o '$path' '$urlBase/$filename'`;
244     }
245 }
246
247 sub findMacOSXVersion()
248 {
249     my $version;
250     open(SW_VERS, "-|", "/usr/bin/sw_vers") || die;
251     while (<SW_VERS>) {
252         $version = $1 if /^ProductVersion:\s+([^\s]+)/;
253     }
254     close(SW_VERS);
255     return $version;
256 }
257
258 sub findNearestNightlyIndex(\@$$)
259 {
260     my ($nightlies, $revision, $round) = @_;
261
262     my $lowIndex = 0;
263     my $highIndex = $#{$nightlies};
264
265     return $highIndex if uc($revision) eq 'HEAD' || $revision >= $nightlies->[$highIndex]->{rev};
266     return $lowIndex if $revision <= $nightlies->[$lowIndex]->{rev};
267
268     while (abs($highIndex - $lowIndex) > 1) {
269         my $index = $lowIndex + int(($highIndex - $lowIndex) / 2);
270         if ($revision < $nightlies->[$index]->{rev}) {
271             $highIndex = $index;
272         } elsif ($revision > $nightlies->[$index]->{rev}) {
273             $lowIndex = $index;
274         } else {
275             return $index;
276         }
277     }
278
279     return ($round eq "floor") ? $lowIndex : $highIndex;
280 }
281
282 sub findSafariVersion($)
283 {
284     my ($path) = @_;
285     my $versionPlist = File::Spec->catdir(dirname(dirname($path)), "version.plist");
286     my $version;
287     open(PLIST, "< $versionPlist") || die;
288     while (<PLIST>) {
289         if (m#^\s*<key>CFBundleShortVersionString</key>#) {
290             $version = <PLIST>;
291             $version =~ s#^\s*<string>([0-9.]+)[^<]*</string>\s*[\r\n]*#$1#;
292         }
293     }
294     close(PLIST);
295     return $version;
296 }
297
298 sub loadSettings()
299 {
300     package Settings;
301
302     our $branch = "trunk";
303     our $nightlyDownloadDirectory = File::Spec->catdir($ENV{HOME}, "Library/Caches/WebKit-Nightlies");
304     our $safariPath = "/Applications/Safari.app";
305
306     my $rcfile = File::Spec->catdir($ENV{HOME}, ".bisect-buildsrc");
307     return if !-f $rcfile;
308
309     my $result = do $rcfile;
310     die "Could not parse $rcfile: $@" if $@;
311 }
312
313 sub makeNightlyList($$$$)
314 {
315     my ($useLocalFiles, $localDirectory, $osxVersionString, $safariVersionString) = @_;
316     my @files;
317
318     if ($useLocalFiles) {
319         opendir(DIR, $localDirectory) || die "$!";
320         foreach my $file (readdir(DIR)) {
321             if ($file =~ /^WebKit-SVN-r([0-9]+)\.dmg$/) {
322                 push(@files, +{ rev => $1, file => $file });
323             }
324         }
325         closedir(DIR);
326     } else {
327         open(NIGHTLIES, "curl -s $nightlyBuildsURLBase/all |") || die;
328
329         while (my $line = <NIGHTLIES>) {
330             chomp $line;
331             my ($revision, $timestamp, $url) = split(/,/, $line);
332             my $nightly = basename($url);
333             push(@files, +{ rev => $revision, file => $nightly });
334         }
335         close(NIGHTLIES);
336     }
337
338     my $osxVersion = eval("v$osxVersionString");
339     my $safariVersion = eval("v$safariVersionString");
340
341     if ($osxVersion ge v10.10 && $osxVersion lt v10.11) {
342         @files = grep { $_->{rev} >= 174650 } @files;
343     } elsif ($osxVersion ge v10.9 && $osxVersion lt v10.10) {
344         @files = grep { $_->{rev} >= 157846 } @files;
345     } elsif ($osxVersion ge v10.8 && $osxVersion lt v10.9) {
346         @files = grep { $_->{rev} >= 122421 } @files;
347     } elsif ($osxVersion ge v10.7 && $osxVersion lt v10.8) {
348         # FIXME: Add filter for 10.7.x
349     } elsif ($osxVersion ge v10.6 && $osxVersion lt v10.7) {
350         # FIXME: Add filter for 10.6.x
351     } elsif ($osxVersion ge v10.5 && $osxVersion lt v10.6) {
352         if ($safariVersionString eq "4 Public Beta") {
353             @files = grep { $_->{rev} >= 39682 } @files;
354         } elsif ($safariVersion ge v3.2) {
355             @files = grep { $_->{rev} >= 37348 } @files;
356         } elsif ($safariVersion ge v3.1) {
357             @files = grep { $_->{rev} >= 29711 } @files;
358         } elsif ($safariVersion ge v3.0) {
359             @files = grep { $_->{rev} >= 25124 } @files;
360         } elsif ($safariVersion ge v2.0) {
361             @files = grep { $_->{rev} >= 19594 } @files;
362         } else {
363             die "Requires Safari 2.0 or newer";
364         }
365     } elsif ($osxVersion ge v10.4 && $osxVersion lt v10.5) {
366         if ($safariVersionString eq "4 Public Beta") {
367             @files = grep { $_->{rev} >= 39682 } @files;
368         } elsif ($safariVersion ge v3.2) {
369             @files = grep { $_->{rev} >= 37348 } @files;
370         } elsif ($safariVersion ge v3.1) {
371             @files = grep { $_->{rev} >= 29711 } @files;
372         } elsif ($safariVersion ge v3.0) {
373             @files = grep { $_->{rev} >= 19992 } @files;
374         } elsif ($safariVersion ge v2.0) {
375             @files = grep { $_->{rev} >= 11976 } @files;
376         } else {
377             die "Requires Safari 2.0 or newer";
378         }
379     } else {
380         die "Requires Mac OS X 10.4 (Tiger) or later";
381     }
382
383     my $nightlycmp = sub { return $a->{rev} <=> $b->{rev}; };
384
385     return sort $nightlycmp @files;
386 }
387
388 sub mountAndRunNightly($$$$)
389 {
390     my ($filename, $directory, $safari, $tempFile) = @_;
391     my $mountPath = "/Volumes/WebKit";
392     my $webkitApp = File::Spec->catfile($mountPath, "WebKit.app");
393     my $diskImage = File::Spec->catfile($directory, $filename);
394     my $devNull = File::Spec->devnull();
395
396     my $i = 0;
397     while (-e $mountPath) {
398         $i++;
399         usleep 100 if $i > 1;
400         `hdiutil detach '$mountPath' 2> $devNull`;
401         die "Could not unmount $diskImage at $mountPath" if $i > 100;
402     }
403     die "Can't mount $diskImage: $mountPath already exists!" if -e $mountPath;
404
405     print "Mounting disk image and running WebKit...\n";
406     `hdiutil attach '$diskImage'`;
407     $i = 0;
408     while (! -e $webkitApp) {
409         usleep 100;
410         $i++;
411         die "Could not mount $diskImage at $mountPath" if $i > 100;
412     }
413
414     my $frameworkPath;
415     if (-d "/Volumes/WebKit/WebKit.app/Contents/Frameworks") {
416         my $osXVersion = join('.', (split(/\./, findMacOSXVersion()))[0..1]);
417         $frameworkPath = "/Volumes/WebKit/WebKit.app/Contents/Frameworks/$osXVersion";
418     } else {
419         $frameworkPath = "/Volumes/WebKit/WebKit.app/Contents/Resources";
420     }
421
422     $tempFile ||= "";
423
424     {
425         local %ENV = %ENV;
426         setupMacWebKitEnvironment($frameworkPath);
427
428         `$safari $tempFile`;
429     }
430
431     `hdiutil detach '$mountPath' 2> $devNull`;
432 }
433
434 sub parseRevisions($$;$)
435 {
436     my ($optionName, $value, $ignored) = @_;
437
438     if ($value =~ /^r?([0-9]+|HEAD):?$/i) {
439         push(@revisions, $1);
440         die "Too many revision arguments specified" if scalar @revisions > 2;
441     } elsif ($value =~ /^r?([0-9]+):?r?([0-9]+|HEAD)$/i) {
442         $revisions[0] = $1;
443         $revisions[1] = $2;
444     } else {
445         die "Unknown revision '$value':  expected 'M' or 'M:N'";
446     }
447 }
448
449 sub printStatus($$$)
450 {
451     my ($startRevision, $endRevision, $isProgression) = @_;
452     printf "\n%s: r%s  %s: r%s\n",
453         $isProgression ? "Fails" : "Works", $startRevision,
454         $isProgression ? "Works" : "Fails", $endRevision;
455 }
456
457 sub printTracLink($$)
458 {
459     my ($startRevision, $endRevision) = @_;
460     if ($startRevision + 1 == $endRevision) {
461         printf("http://trac.webkit.org/changeset/%s\n", $endRevision);
462     } else {
463         printf("http://trac.webkit.org/log/trunk/?rev=%s&stop_rev=%s\n", $endRevision, $startRevision + 1);
464     }
465 }
466
467 sub promptForTest($)
468 {
469     my ($revision) = @_;
470     print "Did the bug reproduce in r$revision (yes/no/broken)? ";
471     my $answer = <STDIN>;
472     return 1 if $answer =~ /^(1|y.*)$/i;
473     return -1 if $answer =~ /^(-1|b.*)$/i; # Broken
474     return 0;
475 }
476