cb9ae548a644a9865eae8021806f043d9f02f8f1
[WebKit-https.git] / WebKitTools / Scripts / bisect-builds
1 #!/usr/bin/perl -w
2
3 # Copyright (C) 2007 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 Computer, 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;
46 use Getopt::Long;
47 use Time::HiRes qw(usleep);
48
49 sub createTempFile($);
50 sub downloadNightly($$$);
51 sub findMacOSXVersion();
52 sub findNearestNightlyIndex(\@$$);
53 sub findSafariVersion($);
54 sub loadSettings();
55 sub makeNightlyList($$$$);
56 sub mountAndRunNightly($$$$);
57 sub parseRevisions($$;$);
58 sub printStatus($$$);
59 sub promptForTest($);
60
61 loadSettings();
62
63 my %validBranches = map { $_ => 1 } qw(feature-branch trunk);
64 my $branch = $Settings::branch;
65 my $nightlyDownloadDirectory = $Settings::nightlyDownloadDirectory;
66 my $safariPath = $Settings::safariPath;
67
68 my @nightlies;
69
70 my $isProgression;
71 my $localOnly;
72 my @revisions;
73 my $sanityCheck;
74 my $showHelp;
75 my $testURL;
76
77 # Fix up -r switches in @ARGV
78 @ARGV = map { /^(-r)(.+)$/ ? ($1, $2) : $_ } @ARGV;
79
80 my $result = GetOptions(
81     "b|branch=s"             => \$branch,
82     "d|download-directory=s" => \$nightlyDownloadDirectory,
83     "h|help"                 => \$showHelp,
84     "l|local!"               => \$localOnly,
85     "p|progression!"         => \$isProgression,
86     "r|revisions=s"          => \&parseRevisions,
87     "safari-path=s"          => \$safariPath,
88     "s|sanity-check!"        => \$sanityCheck,
89 );
90 $testURL = shift @ARGV;
91
92 $branch = "feature-branch" if $branch eq "feature";
93 if (!exists $validBranches{$branch}) {
94     print STDERR "ERROR: Invalid branch '$branch'\n";
95     $showHelp = 1;
96 }
97
98 if (!$result || $showHelp || scalar(@ARGV) > 0) {
99     print STDERR "Search WebKit nightly builds for changes in behavior.\n";
100     print STDERR "Usage: " . basename($0) . " [options] [url]\n";
101     print STDERR <<END;
102   [-b|--branch name]             name of the nightly build branch (default: trunk)
103   [-d|--download-directory dir]  nightly build download directory (default: ~/Library/Caches/WebKit-Nightlies)
104   [-h|--help]                    show this help message
105   [-l|--local]                   only use local (already downloaded) nightlies
106   [-p|--progression]             searching for a progression, not a regression
107   [-r|--revision M[:N]]          specify starting (and optional ending) revisions to search
108   [--safari-path path]           path to Safari application bundle (default: /Applications/Safari.app)
109   [-s|--sanity-check]            verify both starting and ending revisions before bisecting
110 END
111     exit 1;
112 }
113
114 my $nightlyWebSite = "http://nightly.webkit.org";
115 my $nightlyBuildsURLBase = $nightlyWebSite . File::Spec->catdir("/builds", $branch, "mac");
116 my $nightlyFilesURLBase = $nightlyWebSite . File::Spec->catdir("/files", $branch, "mac");
117
118 $nightlyDownloadDirectory = glob($nightlyDownloadDirectory) if $nightlyDownloadDirectory =~ /^~/;
119 $safariPath = glob($safariPath) if $safariPath =~ /^~/;
120 $safariPath = File::Spec->catdir($safariPath, "Contents/MacOS/Safari") if $safariPath =~ m#\.app/*#;
121
122 $nightlyDownloadDirectory = File::Spec->catdir($nightlyDownloadDirectory, $branch);
123 if (! -d $nightlyDownloadDirectory) {
124     mkpath($nightlyDownloadDirectory, 0, 0755) || die "Could not create $nightlyDownloadDirectory: $!";
125 }
126
127 @nightlies = makeNightlyList($localOnly, $nightlyDownloadDirectory, findMacOSXVersion(), findSafariVersion($safariPath));
128
129 my $startIndex = $revisions[0] ? findNearestNightlyIndex(@nightlies, $revisions[0], 'ceil') : 0;
130 my $endIndex = $revisions[1] ? findNearestNightlyIndex(@nightlies, $revisions[1], 'floor') : $#nightlies;
131
132 my $tempFile = createTempFile($testURL);
133
134 if ($sanityCheck) {
135     my $didReproduceBug;
136
137     do {
138         printf "\nChecking starting revision (r%s)...\n",
139             $nightlies[$startIndex]->{rev};
140         downloadNightly($nightlies[$startIndex]->{file}, $nightlyFilesURLBase, $nightlyDownloadDirectory);
141         mountAndRunNightly($nightlies[$startIndex]->{file}, $nightlyDownloadDirectory, $safariPath, $tempFile);
142         $didReproduceBug = promptForTest($nightlies[$startIndex]->{rev});
143         $startIndex-- if $didReproduceBug < 0;
144     } while ($didReproduceBug < 0);
145     die "ERROR: Bug reproduced in starting revision!  Do you need to test an earlier revision or for a progression?"
146         if $didReproduceBug && !$isProgression;
147     die "ERROR: Bug not reproduced in starting revision!  Do you need to test an earlier revision or for a regression?"
148         if !$didReproduceBug && $isProgression;
149
150     do {
151         printf "\nChecking ending revision (r%s)...\n",
152             $nightlies[$endIndex]->{rev};
153         downloadNightly($nightlies[$endIndex]->{file}, $nightlyFilesURLBase, $nightlyDownloadDirectory);
154         mountAndRunNightly($nightlies[$endIndex]->{file}, $nightlyDownloadDirectory, $safariPath, $tempFile);
155         $didReproduceBug = promptForTest($nightlies[$endIndex]->{rev});
156         $endIndex++ if $didReproduceBug < 0;
157     } while ($didReproduceBug < 0);
158     die "ERROR: Bug NOT reproduced in ending revision!  Do you need to test a later revision or for a progression?"
159         if !$didReproduceBug && !$isProgression;
160     die "ERROR: Bug reproduced in ending revision!  Do you need to test a later revision or for a regression?"
161         if $didReproduceBug && $isProgression;
162 }
163
164 printStatus($nightlies[$startIndex]->{rev}, $nightlies[$endIndex]->{rev}, $isProgression);
165
166 my %brokenRevisions = ();
167 while (abs($endIndex - $startIndex) > 1) {
168     my $index = $startIndex + int(($endIndex - $startIndex) / 2);
169
170     my $didReproduceBug;
171     do {
172         if (exists $nightlies[$index]) {
173             printf "\nChecking revision (r%s)...\n", $nightlies[$index]->{rev};
174             downloadNightly($nightlies[$index]->{file}, $nightlyFilesURLBase, $nightlyDownloadDirectory);
175             mountAndRunNightly($nightlies[$index]->{file}, $nightlyDownloadDirectory, $safariPath, $tempFile);
176             $didReproduceBug = promptForTest($nightlies[$index]->{rev});
177         }
178         if ($didReproduceBug < 0) {
179             $brokenRevisions{$nightlies[$index]->{rev}} = $nightlies[$index]->{file};
180             delete $nightlies[$index];
181             $endIndex--;
182             $index = $startIndex + int(($endIndex - $startIndex) / 2);
183         }
184     } while ($didReproduceBug < 0);
185
186     if ($didReproduceBug && !$isProgression || !$didReproduceBug && $isProgression) {
187         $endIndex = $index;
188     } else {
189         $startIndex = $index;
190     }
191
192     print "\nBroken revisions skipped: r" . join(", r", keys %brokenRevisions) . "\n"
193         if scalar keys %brokenRevisions > 0;
194     printStatus($nightlies[$startIndex]->{rev}, $nightlies[$endIndex]->{rev}, $isProgression);
195 }
196
197 unlink $tempFile if $tempFile;
198
199 exit 0;
200
201 sub createTempFile($)
202 {
203     my ($url) = @_;
204
205     return undef if !$url;
206
207     my $fh = new File::Temp(
208         DIR => ($ENV{'TMPDIR'} || "/tmp"),
209         SUFFIX => ".html",
210         TEMPLATE => basename($0) . "-XXXXXXXX",
211         UNLINK => 0,
212     );
213     my $tempFile = $fh->filename();
214     print $fh "<meta http-equiv=\"refresh\" content=\"0; $url\">\n";
215     close($fh);
216
217     return $tempFile;
218 }
219
220 sub downloadNightly($$$)
221 {
222     my ($filename, $urlBase, $directory) = @_;
223     my $path = File::Spec->catfile($directory, $filename);
224     if (! -f $path) {
225         print "Downloading $filename to $directory...\n";
226         `curl -# -o '$path' '$urlBase/$filename'`;
227     }
228 }
229
230 sub findMacOSXVersion()
231 {
232     my $version;
233     open(SW_VERS, "-|", "/usr/bin/sw_vers") || die;
234     while (<SW_VERS>) {
235         $version = $1 if /^ProductVersion:\s+([^\s]+)/;
236     }
237     close(SW_VERS);
238     return $version;
239 }
240
241 sub findNearestNightlyIndex(\@$$)
242 {
243     my ($nightlies, $revision, $round) = @_;
244
245     my $lowIndex = 0;
246     my $highIndex = $#{$nightlies};
247
248     return $highIndex if uc($revision) eq 'HEAD' || $revision >= $nightlies->[$highIndex]->{rev};
249     return $lowIndex if $revision <= $nightlies->[$lowIndex]->{rev};
250
251     while (abs($highIndex - $lowIndex) > 1) {
252         my $index = $lowIndex + int(($highIndex - $lowIndex) / 2);
253         if ($revision < $nightlies->[$index]->{rev}) {
254             $highIndex = $index;
255         } elsif ($revision > $nightlies->[$index]->{rev}) {
256             $lowIndex = $index;
257         } else {
258             return $index;
259         }
260     }
261
262     return ($round eq "floor") ? $lowIndex : $highIndex;
263 }
264
265 sub findSafariVersion($)
266 {
267     my ($path) = @_;
268     my $versionPlist = File::Spec->catdir(dirname(dirname($path)), "version.plist");
269     my $version;
270     open(PLIST, "< $versionPlist") || die;
271     while (<PLIST>) {
272         if (m#^\s*<key>CFBundleShortVersionString</key>#) {
273             $version = <PLIST>;
274             $version =~ s#^\s*<string>([0-9.]+)[^<]*</string>\s*[\r\n]*#$1#;
275         }
276     }
277     close(PLIST);
278     return $version;
279 }
280
281 sub loadSettings()
282 {
283     package Settings;
284
285     our $branch = "trunk";
286     our $nightlyDownloadDirectory = File::Spec->catdir($ENV{HOME}, "Library/Caches/WebKit-Nightlies");
287     our $safariPath = "/Applications/Safari.app";
288
289     my $rcfile = File::Spec->catdir($ENV{HOME}, ".bisect-buildsrc");
290     return if !-f $rcfile;
291
292     my $result = do $rcfile;
293     die "Could not parse $rcfile: $@" if $@;
294 }
295
296 sub makeNightlyList($$$$)
297 {
298     my ($useLocalFiles, $localDirectory, $macOSXVersion, $safariVersion) = @_;
299     my @files;
300
301     if ($useLocalFiles) {
302         opendir(DIR, $localDirectory) || die "$!";
303         foreach my $file (readdir(DIR)) {
304             if ($file =~ /^WebKit-SVN-r([0-9]+)\.dmg$/) {
305                 push(@files, +{ rev => $1, file => $file });
306             }
307         }
308         closedir(DIR);
309     } else {
310         open(NIGHTLIES, "curl -s $nightlyBuildsURLBase/all |") || die;
311
312         while (my $line = <NIGHTLIES>) {
313             chomp $line;
314             my ($revision, $timestamp, $url) = split(/,/, $line);
315             my $nightly = basename($url);
316             push(@files, +{ rev => $revision, file => $nightly });
317         }
318         close(NIGHTLIES);
319     }
320
321     if (eval "v$macOSXVersion" ge v10.5) {
322         if (eval "v$safariVersion" ge v3.1) {
323             @files = grep { $_->{rev} >= 29711 } @files;
324         } elsif (eval "v$safariVersion" ge v3.0) {
325             @files = grep { $_->{rev} >= 25124 } @files;
326         } elsif (eval "v$safariVersion" ge v2.0) {
327             @files = grep { $_->{rev} >= 19594 } @files;
328         } else {
329             die "Requires Safari 2.0 or newer";
330         }
331     } elsif (eval "v$macOSXVersion" ge v10.4) {
332         if (eval "v$safariVersion" ge v3.1) {
333             @files = grep { $_->{rev} >= 29711 } @files;
334         } elsif (eval "v$safariVersion" ge v3.0) {
335             @files = grep { $_->{rev} >= 19992 } @files;
336         } elsif (eval "v$safariVersion" ge v2.0) {
337             @files = grep { $_->{rev} >= 11976 } @files;
338         } else {
339             die "Requires Safari 2.0 or newer";
340         }
341     } else {
342         die "Requires Mac OS X 10.4 (Tiger) or 10.5 (Leopard)";
343     }
344
345     my $nightlycmp = sub { return $a->{rev} <=> $b->{rev}; };
346
347     return sort $nightlycmp @files;
348 }
349
350 sub mountAndRunNightly($$$$)
351 {
352     my ($filename, $directory, $safari, $tempFile) = @_;
353     my $mountPath = "/Volumes/WebKit";
354     my $webkitApp = File::Spec->catfile($mountPath, "WebKit.app");
355     my $diskImage = File::Spec->catfile($directory, $filename);
356
357     my $i = 0;
358     while (-e $mountPath) {
359         $i++;
360         usleep 100 if $i > 1;
361         `hdiutil detach '$mountPath' 2> /dev/null`;
362         die "Could not unmount $diskImage at $mountPath" if $i > 100;
363     }
364     die "Can't mount $diskImage: $mountPath already exists!" if -e $mountPath;
365
366     print "Mounting disk image and running WebKit...\n";
367     `hdiutil attach '$diskImage'`;
368     $i = 0;
369     while (! -e $webkitApp) {
370         usleep 100;
371         $i++;
372         die "Could not mount $diskImage at $mountPath" if $i > 100;
373     }
374
375     my $frameworkPath;
376     if (-d "/Volumes/WebKit/WebKit.app/Contents/Frameworks") {
377         my $osXVersion = join('.', (split(/\./, findMacOSXVersion()))[0..1]);
378         $frameworkPath = "/Volumes/WebKit/WebKit.app/Contents/Frameworks/$osXVersion";
379     } else {
380         $frameworkPath = "/Volumes/WebKit/WebKit.app/Contents/Resources";
381     }
382
383     $tempFile ||= "";
384     `DYLD_FRAMEWORK_PATH=$frameworkPath WEBKIT_UNSET_DYLD_FRAMEWORK_PATH=YES $safari $tempFile`;
385
386     `hdiutil detach '$mountPath' 2> /dev/null`;
387 }
388
389 sub parseRevisions($$;$)
390 {
391     my ($optionName, $value, $ignored) = @_;
392
393     if ($value =~ /^r?([0-9]+|HEAD):?$/i) {
394         push(@revisions, $1);
395         die "Too many revision arguments specified" if scalar @revisions > 2;
396     } elsif ($value =~ /^r?([0-9]+):?r?([0-9]+|HEAD)$/i) {
397         $revisions[0] = $1;
398         $revisions[1] = $2;
399     } else {
400         die "Unknown revision '$value':  expected 'M' or 'M:N'";
401     }
402 }
403
404 sub printStatus($$$)
405 {
406     my ($startRevision, $endRevision, $isProgression) = @_;
407     printf "\n%s: r%s  %s: r%s\n",
408         $isProgression ? "Fails" : "Works", $startRevision,
409         $isProgression ? "Works" : "Fails", $endRevision;
410 }
411
412 sub promptForTest($)
413 {
414     my ($revision) = @_;
415     print "Did the bug reproduce in r$revision (yes/no/broken)? ";
416     my $answer = <STDIN>;
417     return 1 if $answer =~ /^(1|y.*)$/i;
418     return -1 if $answer =~ /^(-1|b.*)$/i; # Broken
419     return 0;
420 }
421