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