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