bisect-builds 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. #!/usr/bin/perl -w
  2. # Copyright (C) 2007, 2008, 2011 Apple Inc. All rights reserved.
  3. #
  4. # Redistribution and use in source and binary forms, with or without
  5. # modification, are permitted provided that the following conditions
  6. # are met:
  7. #
  8. # 1. Redistributions of source code must retain the above copyright
  9. # notice, this list of conditions and the following disclaimer.
  10. # 2. Redistributions in binary form must reproduce the above copyright
  11. # notice, this list of conditions and the following disclaimer in the
  12. # documentation and/or other materials provided with the distribution.
  13. # 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
  14. # its contributors may be used to endorse or promote products derived
  15. # from this software without specific prior written permission.
  16. #
  17. # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
  18. # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  19. # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  20. # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
  21. # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  22. # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  23. # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  24. # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  25. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  26. # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  27. # This script attempts to find the point at which a regression (or progression)
  28. # of behavior occurred by searching WebKit nightly builds.
  29. # To override the location where the nightly builds are downloaded or the path
  30. # to the Safari web browser, create a ~/.bisect-buildsrc file with one or more of
  31. # the following lines (use "~/" to specify a path from your home directory):
  32. #
  33. # $branch = "branch-name";
  34. # $nightlyDownloadDirectory = "~/path/to/nightly/downloads";
  35. # $safariPath = "/path/to/Safari.app";
  36. use strict;
  37. use File::Basename;
  38. use File::Path;
  39. use File::Spec;
  40. use File::Temp qw(tempfile);
  41. use FindBin;
  42. use Getopt::Long;
  43. use Time::HiRes qw(usleep);
  44. use lib $FindBin::Bin;
  45. use webkitdirs qw(safariPathFromSafariBundle);
  46. sub createTempFile($);
  47. sub downloadNightly($$$);
  48. sub findMacOSXVersion();
  49. sub findNearestNightlyIndex(\@$$);
  50. sub findSafariVersion($);
  51. sub loadSettings();
  52. sub makeNightlyList($$$$);
  53. sub max($$) { return $_[0] > $_[1] ? $_[0] : $_[1]; }
  54. sub mountAndRunNightly($$$$);
  55. sub parseRevisions($$;$);
  56. sub printStatus($$$);
  57. sub printTracLink($$);
  58. sub promptForTest($);
  59. loadSettings();
  60. my %validBranches = map { $_ => 1 } qw(feature-branch trunk);
  61. my $branch = $Settings::branch;
  62. my $nightlyDownloadDirectory = $Settings::nightlyDownloadDirectory;
  63. my $safariPath = $Settings::safariPath;
  64. my @nightlies;
  65. my $isProgression;
  66. my $localOnly;
  67. my @revisions;
  68. my $sanityCheck;
  69. my $showHelp;
  70. my $testURL;
  71. # Fix up -r switches in @ARGV
  72. @ARGV = map { /^(-r)(.+)$/ ? ($1, $2) : $_ } @ARGV;
  73. my $result = GetOptions(
  74. "b|branch=s" => \$branch,
  75. "d|download-directory=s" => \$nightlyDownloadDirectory,
  76. "h|help" => \$showHelp,
  77. "l|local!" => \$localOnly,
  78. "p|progression!" => \$isProgression,
  79. "r|revisions=s" => \&parseRevisions,
  80. "safari-path=s" => \$safariPath,
  81. "s|sanity-check!" => \$sanityCheck,
  82. );
  83. $testURL = shift @ARGV;
  84. $branch = "feature-branch" if $branch eq "feature";
  85. if (!exists $validBranches{$branch}) {
  86. print STDERR "ERROR: Invalid branch '$branch'\n";
  87. $showHelp = 1;
  88. }
  89. if (!$result || $showHelp || scalar(@ARGV) > 0) {
  90. print STDERR "Search WebKit nightly builds for changes in behavior.\n";
  91. print STDERR "Usage: " . basename($0) . " [options] [url]\n";
  92. print STDERR <<END;
  93. [-b|--branch name] name of the nightly build branch (default: trunk)
  94. [-d|--download-directory dir] nightly build download directory (default: ~/Library/Caches/WebKit-Nightlies)
  95. [-h|--help] show this help message
  96. [-l|--local] only use local (already downloaded) nightlies
  97. [-p|--progression] searching for a progression, not a regression
  98. [-r|--revision M[:N]] specify starting (and optional ending) revisions to search
  99. [--safari-path path] path to Safari application bundle (default: /Applications/Safari.app)
  100. [-s|--sanity-check] verify both starting and ending revisions before bisecting
  101. END
  102. exit 1;
  103. }
  104. my $nightlyWebSite = "http://nightly.webkit.org";
  105. my $nightlyBuildsURLBase = $nightlyWebSite . File::Spec->catdir("/builds", $branch, "mac");
  106. my $nightlyFilesURLBase = $nightlyWebSite . File::Spec->catdir("/files", $branch, "mac");
  107. $nightlyDownloadDirectory = glob($nightlyDownloadDirectory) if $nightlyDownloadDirectory =~ /^~/;
  108. $safariPath = glob($safariPath) if $safariPath =~ /^~/;
  109. $safariPath = safariPathFromSafariBundle($safariPath) if $safariPath =~ m#\.app/*#;
  110. $nightlyDownloadDirectory = File::Spec->catdir($nightlyDownloadDirectory, $branch);
  111. if (! -d $nightlyDownloadDirectory) {
  112. mkpath($nightlyDownloadDirectory, 0, 0755) || die "Could not create $nightlyDownloadDirectory: $!";
  113. }
  114. @nightlies = makeNightlyList($localOnly, $nightlyDownloadDirectory, findMacOSXVersion(), findSafariVersion($safariPath));
  115. my $startIndex = $revisions[0] ? findNearestNightlyIndex(@nightlies, $revisions[0], 'ceil') : 0;
  116. my $endIndex = $revisions[1] ? findNearestNightlyIndex(@nightlies, $revisions[1], 'floor') : $#nightlies;
  117. my $tempFile = createTempFile($testURL);
  118. if ($sanityCheck) {
  119. my $didReproduceBug;
  120. do {
  121. printf "\nChecking starting revision r%s...\n",
  122. $nightlies[$startIndex]->{rev};
  123. downloadNightly($nightlies[$startIndex]->{file}, $nightlyFilesURLBase, $nightlyDownloadDirectory);
  124. mountAndRunNightly($nightlies[$startIndex]->{file}, $nightlyDownloadDirectory, $safariPath, $tempFile);
  125. $didReproduceBug = promptForTest($nightlies[$startIndex]->{rev});
  126. $startIndex-- if $didReproduceBug < 0;
  127. } while ($didReproduceBug < 0);
  128. die "ERROR: Bug reproduced in starting revision! Do you need to test an earlier revision or for a progression?"
  129. if $didReproduceBug && !$isProgression;
  130. die "ERROR: Bug not reproduced in starting revision! Do you need to test an earlier revision or for a regression?"
  131. if !$didReproduceBug && $isProgression;
  132. do {
  133. printf "\nChecking ending revision r%s...\n",
  134. $nightlies[$endIndex]->{rev};
  135. downloadNightly($nightlies[$endIndex]->{file}, $nightlyFilesURLBase, $nightlyDownloadDirectory);
  136. mountAndRunNightly($nightlies[$endIndex]->{file}, $nightlyDownloadDirectory, $safariPath, $tempFile);
  137. $didReproduceBug = promptForTest($nightlies[$endIndex]->{rev});
  138. $endIndex++ if $didReproduceBug < 0;
  139. } while ($didReproduceBug < 0);
  140. die "ERROR: Bug NOT reproduced in ending revision! Do you need to test a later revision or for a progression?"
  141. if !$didReproduceBug && !$isProgression;
  142. die "ERROR: Bug reproduced in ending revision! Do you need to test a later revision or for a regression?"
  143. if $didReproduceBug && $isProgression;
  144. }
  145. printStatus($nightlies[$startIndex]->{rev}, $nightlies[$endIndex]->{rev}, $isProgression);
  146. my %brokenRevisions = ();
  147. while (abs($endIndex - $startIndex) > 1) {
  148. my $index = $startIndex + int(($endIndex - $startIndex) / 2);
  149. my $didReproduceBug;
  150. do {
  151. if (exists $nightlies[$index]) {
  152. my $buildsLeft = max(max(0, $endIndex - $index - 1), max(0, $index - $startIndex - 1));
  153. my $plural = $buildsLeft == 1 ? "" : "s";
  154. printf "\nChecking revision r%s (%d build%s left to test after this)...\n", $nightlies[$index]->{rev}, $buildsLeft, $plural;
  155. downloadNightly($nightlies[$index]->{file}, $nightlyFilesURLBase, $nightlyDownloadDirectory);
  156. mountAndRunNightly($nightlies[$index]->{file}, $nightlyDownloadDirectory, $safariPath, $tempFile);
  157. $didReproduceBug = promptForTest($nightlies[$index]->{rev});
  158. }
  159. if ($didReproduceBug < 0) {
  160. $brokenRevisions{$nightlies[$index]->{rev}} = $nightlies[$index]->{file};
  161. delete $nightlies[$index];
  162. $endIndex--;
  163. $index = $startIndex + int(($endIndex - $startIndex) / 2);
  164. }
  165. } while ($didReproduceBug < 0);
  166. if ($didReproduceBug && !$isProgression || !$didReproduceBug && $isProgression) {
  167. $endIndex = $index;
  168. } else {
  169. $startIndex = $index;
  170. }
  171. print "\nBroken revisions skipped: r" . join(", r", keys %brokenRevisions) . "\n"
  172. if scalar keys %brokenRevisions > 0;
  173. printStatus($nightlies[$startIndex]->{rev}, $nightlies[$endIndex]->{rev}, $isProgression);
  174. }
  175. printTracLink($nightlies[$startIndex]->{rev}, $nightlies[$endIndex]->{rev});
  176. unlink $tempFile if $tempFile;
  177. exit 0;
  178. sub createTempFile($)
  179. {
  180. my ($url) = @_;
  181. return undef if !$url;
  182. my ($fh, $tempFile) = tempfile(
  183. basename($0) . "-XXXXXXXX",
  184. DIR => File::Spec->tmpdir(),
  185. SUFFIX => ".html",
  186. UNLINK => 0,
  187. );
  188. print $fh "<meta http-equiv=\"refresh\" content=\"0; $url\">\n";
  189. close($fh);
  190. return $tempFile;
  191. }
  192. sub downloadNightly($$$)
  193. {
  194. my ($filename, $urlBase, $directory) = @_;
  195. my $path = File::Spec->catfile($directory, $filename);
  196. if (! -f $path) {
  197. print "Downloading $filename to $directory...\n";
  198. `curl -# -o '$path' '$urlBase/$filename'`;
  199. }
  200. }
  201. sub findMacOSXVersion()
  202. {
  203. my $version;
  204. open(SW_VERS, "-|", "/usr/bin/sw_vers") || die;
  205. while (<SW_VERS>) {
  206. $version = $1 if /^ProductVersion:\s+([^\s]+)/;
  207. }
  208. close(SW_VERS);
  209. return $version;
  210. }
  211. sub findNearestNightlyIndex(\@$$)
  212. {
  213. my ($nightlies, $revision, $round) = @_;
  214. my $lowIndex = 0;
  215. my $highIndex = $#{$nightlies};
  216. return $highIndex if uc($revision) eq 'HEAD' || $revision >= $nightlies->[$highIndex]->{rev};
  217. return $lowIndex if $revision <= $nightlies->[$lowIndex]->{rev};
  218. while (abs($highIndex - $lowIndex) > 1) {
  219. my $index = $lowIndex + int(($highIndex - $lowIndex) / 2);
  220. if ($revision < $nightlies->[$index]->{rev}) {
  221. $highIndex = $index;
  222. } elsif ($revision > $nightlies->[$index]->{rev}) {
  223. $lowIndex = $index;
  224. } else {
  225. return $index;
  226. }
  227. }
  228. return ($round eq "floor") ? $lowIndex : $highIndex;
  229. }
  230. sub findSafariVersion($)
  231. {
  232. my ($path) = @_;
  233. my $versionPlist = File::Spec->catdir(dirname(dirname($path)), "version.plist");
  234. my $version;
  235. open(PLIST, "< $versionPlist") || die;
  236. while (<PLIST>) {
  237. if (m#^\s*<key>CFBundleShortVersionString</key>#) {
  238. $version = <PLIST>;
  239. $version =~ s#^\s*<string>([0-9.]+)[^<]*</string>\s*[\r\n]*#$1#;
  240. }
  241. }
  242. close(PLIST);
  243. return $version;
  244. }
  245. sub loadSettings()
  246. {
  247. package Settings;
  248. our $branch = "trunk";
  249. our $nightlyDownloadDirectory = File::Spec->catdir($ENV{HOME}, "Library/Caches/WebKit-Nightlies");
  250. our $safariPath = "/Applications/Safari.app";
  251. my $rcfile = File::Spec->catdir($ENV{HOME}, ".bisect-buildsrc");
  252. return if !-f $rcfile;
  253. my $result = do $rcfile;
  254. die "Could not parse $rcfile: $@" if $@;
  255. }
  256. sub makeNightlyList($$$$)
  257. {
  258. my ($useLocalFiles, $localDirectory, $macOSXVersion, $safariVersion) = @_;
  259. my @files;
  260. if ($useLocalFiles) {
  261. opendir(DIR, $localDirectory) || die "$!";
  262. foreach my $file (readdir(DIR)) {
  263. if ($file =~ /^WebKit-SVN-r([0-9]+)\.dmg$/) {
  264. push(@files, +{ rev => $1, file => $file });
  265. }
  266. }
  267. closedir(DIR);
  268. } else {
  269. open(NIGHTLIES, "curl -s $nightlyBuildsURLBase/all |") || die;
  270. while (my $line = <NIGHTLIES>) {
  271. chomp $line;
  272. my ($revision, $timestamp, $url) = split(/,/, $line);
  273. my $nightly = basename($url);
  274. push(@files, +{ rev => $revision, file => $nightly });
  275. }
  276. close(NIGHTLIES);
  277. }
  278. if (eval "v$macOSXVersion" ge v10.5) {
  279. if ($safariVersion eq "4 Public Beta") {
  280. @files = grep { $_->{rev} >= 39682 } @files;
  281. } elsif (eval "v$safariVersion" ge v3.2) {
  282. @files = grep { $_->{rev} >= 37348 } @files;
  283. } elsif (eval "v$safariVersion" ge v3.1) {
  284. @files = grep { $_->{rev} >= 29711 } @files;
  285. } elsif (eval "v$safariVersion" ge v3.0) {
  286. @files = grep { $_->{rev} >= 25124 } @files;
  287. } elsif (eval "v$safariVersion" ge v2.0) {
  288. @files = grep { $_->{rev} >= 19594 } @files;
  289. } else {
  290. die "Requires Safari 2.0 or newer";
  291. }
  292. } elsif (eval "v$macOSXVersion" ge v10.4) {
  293. if ($safariVersion eq "4 Public Beta") {
  294. @files = grep { $_->{rev} >= 39682 } @files;
  295. } elsif (eval "v$safariVersion" ge v3.2) {
  296. @files = grep { $_->{rev} >= 37348 } @files;
  297. } elsif (eval "v$safariVersion" ge v3.1) {
  298. @files = grep { $_->{rev} >= 29711 } @files;
  299. } elsif (eval "v$safariVersion" ge v3.0) {
  300. @files = grep { $_->{rev} >= 19992 } @files;
  301. } elsif (eval "v$safariVersion" ge v2.0) {
  302. @files = grep { $_->{rev} >= 11976 } @files;
  303. } else {
  304. die "Requires Safari 2.0 or newer";
  305. }
  306. } else {
  307. die "Requires Mac OS X 10.4 (Tiger) or 10.5 (Leopard)";
  308. }
  309. my $nightlycmp = sub { return $a->{rev} <=> $b->{rev}; };
  310. return sort $nightlycmp @files;
  311. }
  312. sub mountAndRunNightly($$$$)
  313. {
  314. my ($filename, $directory, $safari, $tempFile) = @_;
  315. my $mountPath = "/Volumes/WebKit";
  316. my $webkitApp = File::Spec->catfile($mountPath, "WebKit.app");
  317. my $diskImage = File::Spec->catfile($directory, $filename);
  318. my $devNull = File::Spec->devnull();
  319. my $i = 0;
  320. while (-e $mountPath) {
  321. $i++;
  322. usleep 100 if $i > 1;
  323. `hdiutil detach '$mountPath' 2> $devNull`;
  324. die "Could not unmount $diskImage at $mountPath" if $i > 100;
  325. }
  326. die "Can't mount $diskImage: $mountPath already exists!" if -e $mountPath;
  327. print "Mounting disk image and running WebKit...\n";
  328. `hdiutil attach '$diskImage'`;
  329. $i = 0;
  330. while (! -e $webkitApp) {
  331. usleep 100;
  332. $i++;
  333. die "Could not mount $diskImage at $mountPath" if $i > 100;
  334. }
  335. my $frameworkPath;
  336. if (-d "/Volumes/WebKit/WebKit.app/Contents/Frameworks") {
  337. my $osXVersion = join('.', (split(/\./, findMacOSXVersion()))[0..1]);
  338. $frameworkPath = "/Volumes/WebKit/WebKit.app/Contents/Frameworks/$osXVersion";
  339. } else {
  340. $frameworkPath = "/Volumes/WebKit/WebKit.app/Contents/Resources";
  341. }
  342. $tempFile ||= "";
  343. `DYLD_FRAMEWORK_PATH=$frameworkPath WEBKIT_UNSET_DYLD_FRAMEWORK_PATH=YES $safari $tempFile`;
  344. `hdiutil detach '$mountPath' 2> $devNull`;
  345. }
  346. sub parseRevisions($$;$)
  347. {
  348. my ($optionName, $value, $ignored) = @_;
  349. if ($value =~ /^r?([0-9]+|HEAD):?$/i) {
  350. push(@revisions, $1);
  351. die "Too many revision arguments specified" if scalar @revisions > 2;
  352. } elsif ($value =~ /^r?([0-9]+):?r?([0-9]+|HEAD)$/i) {
  353. $revisions[0] = $1;
  354. $revisions[1] = $2;
  355. } else {
  356. die "Unknown revision '$value': expected 'M' or 'M:N'";
  357. }
  358. }
  359. sub printStatus($$$)
  360. {
  361. my ($startRevision, $endRevision, $isProgression) = @_;
  362. printf "\n%s: r%s %s: r%s\n",
  363. $isProgression ? "Fails" : "Works", $startRevision,
  364. $isProgression ? "Works" : "Fails", $endRevision;
  365. }
  366. sub printTracLink($$)
  367. {
  368. my ($startRevision, $endRevision) = @_;
  369. printf("http://trac.webkit.org/log/trunk/?rev=%s&stop_rev=%s\n", $endRevision, $startRevision + 1);
  370. }
  371. sub promptForTest($)
  372. {
  373. my ($revision) = @_;
  374. print "Did the bug reproduce in r$revision (yes/no/broken)? ";
  375. my $answer = <STDIN>;
  376. return 1 if $answer =~ /^(1|y.*)$/i;
  377. return -1 if $answer =~ /^(-1|b.*)$/i; # Broken
  378. return 0;
  379. }