| OLD | NEW |
| (Empty) |
| 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 o
f | |
| 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: trun
k) | |
| 109 [-d|--download-directory dir] nightly build download directory (default: ~/Li
brary/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) revision
s to search | |
| 114 [--safari-path path] path to Safari application bundle (default: /Ap
plications/Safari.app) | |
| 115 [-s|--sanity-check] verify both starting and ending revisions befor
e bisecting | |
| 116 END | |
| 117 exit 1; | |
| 118 } | |
| 119 | |
| 120 my $nightlyWebSite = "http://nightly.webkit.org"; | |
| 121 my $nightlyBuildsURLBase = $nightlyWebSite . File::Spec->catdir("/builds", $bran
ch, "mac"); | |
| 122 my $nightlyFilesURLBase = $nightlyWebSite . File::Spec->catdir("/files", $branch
, "mac"); | |
| 123 | |
| 124 $nightlyDownloadDirectory = glob($nightlyDownloadDirectory) if $nightlyDownloadD
irectory =~ /^~/; | |
| 125 $safariPath = glob($safariPath) if $safariPath =~ /^~/; | |
| 126 $safariPath = safariPathFromSafariBundle($safariPath) if $safariPath =~ m#\.app/
*#; | |
| 127 | |
| 128 $nightlyDownloadDirectory = File::Spec->catdir($nightlyDownloadDirectory, $branc
h); | |
| 129 if (! -d $nightlyDownloadDirectory) { | |
| 130 mkpath($nightlyDownloadDirectory, 0, 0755) || die "Could not create $nightly
DownloadDirectory: $!"; | |
| 131 } | |
| 132 | |
| 133 @nightlies = makeNightlyList($localOnly, $nightlyDownloadDirectory, findMacOSXVe
rsion(), 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}, $nightlyDownloadDire
ctory, $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 ear
lier 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, $ni
ghtlyDownloadDirectory); | |
| 160 mountAndRunNightly($nightlies[$endIndex]->{file}, $nightlyDownloadDirect
ory, $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 la
ter 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}, $isPro
gression); | |
| 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, $n
ightlyDownloadDirectory); | |
| 183 mountAndRunNightly($nightlies[$index]->{file}, $nightlyDownloadDirec
tory, $safariPath, $tempFile); | |
| 184 $didReproduceBug = promptForTest($nightlies[$index]->{rev}); | |
| 185 } | |
| 186 if ($didReproduceBug < 0) { | |
| 187 $brokenRevisions{$nightlies[$index]->{rev}} = $nightlies[$index]->{f
ile}; | |
| 188 delete $nightlies[$index]; | |
| 189 $endIndex--; | |
| 190 $index = $startIndex + int(($endIndex - $startIndex) / 2); | |
| 191 } | |
| 192 } while ($didReproduceBug < 0); | |
| 193 | |
| 194 if ($didReproduceBug && !$isProgression || !$didReproduceBug && $isProgressi
on) { | |
| 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}, $i
sProgression); | |
| 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->[$h
ighIndex]->{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.plis
t"); | |
| 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/Cach
es/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/$osXVer
sion"; | |
| 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 $sa
fari $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", $endRevisio
n, $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 | |
| OLD | NEW |