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