Use libaddressinput revision 282.
[chromium-blink-merge.git] / tools / bisect-builds.py
blobbc65566e1285ef56a6077ca7032ed87d47fae6a7
1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Snapshot Build Bisect Tool
8 This script bisects a snapshot archive using binary search. It starts at
9 a bad revision (it will try to guess HEAD) and asks for a last known-good
10 revision. It will then binary search across this revision range by downloading,
11 unzipping, and opening Chromium for you. After testing the specific revision,
12 it will ask you whether it is good or bad before continuing the search.
13 """
15 # The root URL for storage.
16 CHROMIUM_BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-snapshots'
17 WEBKIT_BASE_URL = 'http://commondatastorage.googleapis.com/chromium-webkit-snapshots'
19 # The root URL for official builds.
20 OFFICIAL_BASE_URL = 'http://master.chrome.corp.google.com/official_builds'
22 # Changelogs URL.
23 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \
24 'perf/dashboard/ui/changelog.html?' \
25 'url=/trunk/src&range=%d%%3A%d'
27 # Official Changelogs URL.
28 OFFICIAL_CHANGELOG_URL = 'http://omahaproxy.appspot.com/'\
29 'changelog?old_version=%s&new_version=%s'
31 # DEPS file URL.
32 DEPS_FILE = 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d'
33 # Blink Changelogs URL.
34 BLINK_CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \
35 'perf/dashboard/ui/changelog_blink.html?' \
36 'url=/trunk&range=%d%%3A%d'
38 DONE_MESSAGE_GOOD_MIN = 'You are probably looking for a change made after %s ' \
39 '(known good), but no later than %s (first known bad).'
40 DONE_MESSAGE_GOOD_MAX = 'You are probably looking for a change made after %s ' \
41 '(known bad), but no later than %s (first known good).'
43 ###############################################################################
45 import json
46 import optparse
47 import os
48 import re
49 import shlex
50 import shutil
51 import subprocess
52 import sys
53 import tempfile
54 import threading
55 import urllib
56 from distutils.version import LooseVersion
57 from xml.etree import ElementTree
58 import zipfile
61 class PathContext(object):
62 """A PathContext is used to carry the information used to construct URLs and
63 paths when dealing with the storage server and archives."""
64 def __init__(self, base_url, platform, good_revision, bad_revision,
65 is_official, is_aura, flash_path = None):
66 super(PathContext, self).__init__()
67 # Store off the input parameters.
68 self.base_url = base_url
69 self.platform = platform # What's passed in to the '-a/--archive' option.
70 self.good_revision = good_revision
71 self.bad_revision = bad_revision
72 self.is_official = is_official
73 self.is_aura = is_aura
74 self.flash_path = flash_path
76 # The name of the ZIP file in a revision directory on the server.
77 self.archive_name = None
79 # Set some internal members:
80 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'.
81 # _archive_extract_dir = Uncompressed directory in the archive_name file.
82 # _binary_name = The name of the executable to run.
83 if self.platform in ('linux', 'linux64', 'linux-arm'):
84 self._binary_name = 'chrome'
85 elif self.platform == 'mac':
86 self.archive_name = 'chrome-mac.zip'
87 self._archive_extract_dir = 'chrome-mac'
88 elif self.platform == 'win':
89 self.archive_name = 'chrome-win32.zip'
90 self._archive_extract_dir = 'chrome-win32'
91 self._binary_name = 'chrome.exe'
92 else:
93 raise Exception('Invalid platform: %s' % self.platform)
95 if is_official:
96 if self.platform == 'linux':
97 self._listing_platform_dir = 'precise32bit/'
98 self.archive_name = 'chrome-precise32bit.zip'
99 self._archive_extract_dir = 'chrome-precise32bit'
100 elif self.platform == 'linux64':
101 self._listing_platform_dir = 'precise64bit/'
102 self.archive_name = 'chrome-precise64bit.zip'
103 self._archive_extract_dir = 'chrome-precise64bit'
104 elif self.platform == 'mac':
105 self._listing_platform_dir = 'mac/'
106 self._binary_name = 'Google Chrome.app/Contents/MacOS/Google Chrome'
107 elif self.platform == 'win':
108 if self.is_aura:
109 self._listing_platform_dir = 'win-aura/'
110 else:
111 self._listing_platform_dir = 'win/'
112 else:
113 if self.platform in ('linux', 'linux64', 'linux-arm'):
114 self.archive_name = 'chrome-linux.zip'
115 self._archive_extract_dir = 'chrome-linux'
116 if self.platform == 'linux':
117 self._listing_platform_dir = 'Linux/'
118 elif self.platform == 'linux64':
119 self._listing_platform_dir = 'Linux_x64/'
120 elif self.platform == 'linux-arm':
121 self._listing_platform_dir = 'Linux_ARM_Cross-Compile/'
122 elif self.platform == 'mac':
123 self._listing_platform_dir = 'Mac/'
124 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium'
125 elif self.platform == 'win':
126 self._listing_platform_dir = 'Win/'
128 def GetListingURL(self, marker=None):
129 """Returns the URL for a directory listing, with an optional marker."""
130 marker_param = ''
131 if marker:
132 marker_param = '&marker=' + str(marker)
133 return self.base_url + '/?delimiter=/&prefix=' + \
134 self._listing_platform_dir + marker_param
136 def GetDownloadURL(self, revision):
137 """Gets the download URL for a build archive of a specific revision."""
138 if self.is_official:
139 return "%s/%s/%s%s" % (
140 OFFICIAL_BASE_URL, revision, self._listing_platform_dir,
141 self.archive_name)
142 else:
143 return "%s/%s%s/%s" % (self.base_url, self._listing_platform_dir,
144 revision, self.archive_name)
146 def GetLastChangeURL(self):
147 """Returns a URL to the LAST_CHANGE file."""
148 return self.base_url + '/' + self._listing_platform_dir + 'LAST_CHANGE'
150 def GetLaunchPath(self):
151 """Returns a relative path (presumably from the archive extraction location)
152 that is used to run the executable."""
153 return os.path.join(self._archive_extract_dir, self._binary_name)
155 def IsAuraBuild(self, build):
156 """Check the given build is Aura."""
157 return build.split('.')[3] == '1'
159 def IsASANBuild(self, build):
160 """Check the given build is ASAN build."""
161 return build.split('.')[3] == '2'
163 def ParseDirectoryIndex(self):
164 """Parses the Google Storage directory listing into a list of revision
165 numbers."""
167 def _FetchAndParse(url):
168 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If
169 next-marker is not None, then the listing is a partial listing and another
170 fetch should be performed with next-marker being the marker= GET
171 parameter."""
172 handle = urllib.urlopen(url)
173 document = ElementTree.parse(handle)
175 # All nodes in the tree are namespaced. Get the root's tag name to extract
176 # the namespace. Etree does namespaces as |{namespace}tag|.
177 root_tag = document.getroot().tag
178 end_ns_pos = root_tag.find('}')
179 if end_ns_pos == -1:
180 raise Exception("Could not locate end namespace for directory index")
181 namespace = root_tag[:end_ns_pos + 1]
183 # Find the prefix (_listing_platform_dir) and whether or not the list is
184 # truncated.
185 prefix_len = len(document.find(namespace + 'Prefix').text)
186 next_marker = None
187 is_truncated = document.find(namespace + 'IsTruncated')
188 if is_truncated is not None and is_truncated.text.lower() == 'true':
189 next_marker = document.find(namespace + 'NextMarker').text
191 # Get a list of all the revisions.
192 all_prefixes = document.findall(namespace + 'CommonPrefixes/' +
193 namespace + 'Prefix')
194 # The <Prefix> nodes have content of the form of
195 # |_listing_platform_dir/revision/|. Strip off the platform dir and the
196 # trailing slash to just have a number.
197 revisions = []
198 for prefix in all_prefixes:
199 revnum = prefix.text[prefix_len:-1]
200 try:
201 revnum = int(revnum)
202 revisions.append(revnum)
203 except ValueError:
204 pass
205 return (revisions, next_marker)
207 # Fetch the first list of revisions.
208 (revisions, next_marker) = _FetchAndParse(self.GetListingURL())
210 # If the result list was truncated, refetch with the next marker. Do this
211 # until an entire directory listing is done.
212 while next_marker:
213 next_url = self.GetListingURL(next_marker)
214 (new_revisions, next_marker) = _FetchAndParse(next_url)
215 revisions.extend(new_revisions)
216 return revisions
218 def GetRevList(self):
219 """Gets the list of revision numbers between self.good_revision and
220 self.bad_revision."""
221 # Download the revlist and filter for just the range between good and bad.
222 minrev = min(self.good_revision, self.bad_revision)
223 maxrev = max(self.good_revision, self.bad_revision)
224 revlist_all = map(int, self.ParseDirectoryIndex())
226 revlist = [x for x in revlist_all if x >= int(minrev) and x <= int(maxrev)]
227 revlist.sort()
229 # Set good and bad revisions to be legit revisions.
230 if revlist:
231 if self.good_revision < self.bad_revision:
232 self.good_revision = revlist[0]
233 self.bad_revision = revlist[-1]
234 else:
235 self.bad_revision = revlist[0]
236 self.good_revision = revlist[-1]
238 # Fix chromium rev so that the deps blink revision matches REVISIONS file.
239 if self.base_url == WEBKIT_BASE_URL:
240 revlist_all.sort()
241 self.good_revision = FixChromiumRevForBlink(revlist,
242 revlist_all,
243 self,
244 self.good_revision)
245 self.bad_revision = FixChromiumRevForBlink(revlist,
246 revlist_all,
247 self,
248 self.bad_revision)
249 return revlist
251 def GetOfficialBuildsList(self):
252 """Gets the list of official build numbers between self.good_revision and
253 self.bad_revision."""
254 # Download the revlist and filter for just the range between good and bad.
255 minrev = min(self.good_revision, self.bad_revision)
256 maxrev = max(self.good_revision, self.bad_revision)
257 handle = urllib.urlopen(OFFICIAL_BASE_URL)
258 dirindex = handle.read()
259 handle.close()
260 build_numbers = re.findall(r'<a href="([0-9][0-9].*)/">', dirindex)
261 final_list = []
262 i = 0
263 parsed_build_numbers = [LooseVersion(x) for x in build_numbers]
264 for build_number in sorted(parsed_build_numbers):
265 path = OFFICIAL_BASE_URL + '/' + str(build_number) + '/' + \
266 self._listing_platform_dir + self.archive_name
267 i = i + 1
268 try:
269 connection = urllib.urlopen(path)
270 connection.close()
271 if build_number > maxrev:
272 break
273 if build_number >= minrev:
274 # If we are bisecting Aura, we want to include only builds which
275 # ends with ".1".
276 if self.is_aura:
277 if self.IsAuraBuild(str(build_number)):
278 final_list.append(str(build_number))
279 # If we are bisecting only official builds (without --aura),
280 # we can not include builds which ends with '.1' or '.2' since
281 # they have different folder hierarchy inside.
282 elif (not self.IsAuraBuild(str(build_number)) and
283 not self.IsASANBuild(str(build_number))):
284 final_list.append(str(build_number))
285 except urllib.HTTPError, e:
286 pass
287 return final_list
289 def UnzipFilenameToDir(filename, directory):
290 """Unzip |filename| to |directory|."""
291 cwd = os.getcwd()
292 if not os.path.isabs(filename):
293 filename = os.path.join(cwd, filename)
294 zf = zipfile.ZipFile(filename)
295 # Make base.
296 if not os.path.isdir(directory):
297 os.mkdir(directory)
298 os.chdir(directory)
299 # Extract files.
300 for info in zf.infolist():
301 name = info.filename
302 if name.endswith('/'): # dir
303 if not os.path.isdir(name):
304 os.makedirs(name)
305 else: # file
306 directory = os.path.dirname(name)
307 if not os.path.isdir(directory):
308 os.makedirs(directory)
309 out = open(name, 'wb')
310 out.write(zf.read(name))
311 out.close()
312 # Set permissions. Permission info in external_attr is shifted 16 bits.
313 os.chmod(name, info.external_attr >> 16L)
314 os.chdir(cwd)
317 def FetchRevision(context, rev, filename, quit_event=None, progress_event=None):
318 """Downloads and unzips revision |rev|.
319 @param context A PathContext instance.
320 @param rev The Chromium revision number/tag to download.
321 @param filename The destination for the downloaded file.
322 @param quit_event A threading.Event which will be set by the master thread to
323 indicate that the download should be aborted.
324 @param progress_event A threading.Event which will be set by the master thread
325 to indicate that the progress of the download should be
326 displayed.
328 def ReportHook(blocknum, blocksize, totalsize):
329 if quit_event and quit_event.isSet():
330 raise RuntimeError("Aborting download of revision %s" % str(rev))
331 if progress_event and progress_event.isSet():
332 size = blocknum * blocksize
333 if totalsize == -1: # Total size not known.
334 progress = "Received %d bytes" % size
335 else:
336 size = min(totalsize, size)
337 progress = "Received %d of %d bytes, %.2f%%" % (
338 size, totalsize, 100.0 * size / totalsize)
339 # Send a \r to let all progress messages use just one line of output.
340 sys.stdout.write("\r" + progress)
341 sys.stdout.flush()
343 download_url = context.GetDownloadURL(rev)
344 try:
345 urllib.urlretrieve(download_url, filename, ReportHook)
346 if progress_event and progress_event.isSet():
347 print
348 except RuntimeError, e:
349 pass
352 def RunRevision(context, revision, zipfile, profile, num_runs, command, args):
353 """Given a zipped revision, unzip it and run the test."""
354 print "Trying revision %s..." % str(revision)
356 # Create a temp directory and unzip the revision into it.
357 cwd = os.getcwd()
358 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
359 UnzipFilenameToDir(zipfile, tempdir)
360 os.chdir(tempdir)
362 # Run the build as many times as specified.
363 testargs = ['--user-data-dir=%s' % profile] + args
364 # The sandbox must be run as root on Official Chrome, so bypass it.
365 if ((context.is_official or context.flash_path) and
366 context.platform.startswith('linux')):
367 testargs.append('--no-sandbox')
368 if context.flash_path:
369 testargs.append('--ppapi-flash-path=%s' % context.flash_path)
370 # We have to pass a large enough Flash version, which currently needs not
371 # be correct. Instead of requiring the user of the script to figure out and
372 # pass the correct version we just spoof it.
373 testargs.append('--ppapi-flash-version=99.9.999.999')
375 runcommand = []
376 for token in shlex.split(command):
377 if token == "%a":
378 runcommand.extend(testargs)
379 else:
380 runcommand.append( \
381 token.replace('%p', os.path.abspath(context.GetLaunchPath())) \
382 .replace('%s', ' '.join(testargs)))
384 results = []
385 for i in range(0, num_runs):
386 subproc = subprocess.Popen(runcommand,
387 bufsize=-1,
388 stdout=subprocess.PIPE,
389 stderr=subprocess.PIPE)
390 (stdout, stderr) = subproc.communicate()
391 results.append((subproc.returncode, stdout, stderr))
393 os.chdir(cwd)
394 try:
395 shutil.rmtree(tempdir, True)
396 except Exception, e:
397 pass
399 for (returncode, stdout, stderr) in results:
400 if returncode:
401 return (returncode, stdout, stderr)
402 return results[0]
405 def AskIsGoodBuild(rev, official_builds, status, stdout, stderr):
406 """Ask the user whether build |rev| is good or bad."""
407 # Loop until we get a response that we can parse.
408 while True:
409 response = raw_input('Revision %s is ' \
410 '[(g)ood/(b)ad/(r)etry/(u)nknown/(q)uit]: ' %
411 str(rev))
412 if response and response in ('g', 'b', 'r', 'u'):
413 return response
414 if response and response == 'q':
415 raise SystemExit()
418 class DownloadJob(object):
419 """DownloadJob represents a task to download a given Chromium revision."""
420 def __init__(self, context, name, rev, zipfile):
421 super(DownloadJob, self).__init__()
422 # Store off the input parameters.
423 self.context = context
424 self.name = name
425 self.rev = rev
426 self.zipfile = zipfile
427 self.quit_event = threading.Event()
428 self.progress_event = threading.Event()
430 def Start(self):
431 """Starts the download."""
432 fetchargs = (self.context,
433 self.rev,
434 self.zipfile,
435 self.quit_event,
436 self.progress_event)
437 self.thread = threading.Thread(target=FetchRevision,
438 name=self.name,
439 args=fetchargs)
440 self.thread.start()
442 def Stop(self):
443 """Stops the download which must have been started previously."""
444 self.quit_event.set()
445 self.thread.join()
446 os.unlink(self.zipfile)
448 def WaitFor(self):
449 """Prints a message and waits for the download to complete. The download
450 must have been started previously."""
451 print "Downloading revision %s..." % str(self.rev)
452 self.progress_event.set() # Display progress of download.
453 self.thread.join()
456 def Bisect(base_url,
457 platform,
458 official_builds,
459 is_aura,
460 good_rev=0,
461 bad_rev=0,
462 num_runs=1,
463 command="%p %a",
464 try_args=(),
465 profile=None,
466 flash_path=None,
467 interactive=True,
468 evaluate=AskIsGoodBuild):
469 """Given known good and known bad revisions, run a binary search on all
470 archived revisions to determine the last known good revision.
472 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.).
473 @param official_builds Specify build type (Chromium or Official build).
474 @param good_rev Number/tag of the known good revision.
475 @param bad_rev Number/tag of the known bad revision.
476 @param num_runs Number of times to run each build for asking good/bad.
477 @param try_args A tuple of arguments to pass to the test application.
478 @param profile The name of the user profile to run with.
479 @param interactive If it is false, use command exit code for good or bad
480 judgment of the argument build.
481 @param evaluate A function which returns 'g' if the argument build is good,
482 'b' if it's bad or 'u' if unknown.
484 Threading is used to fetch Chromium revisions in the background, speeding up
485 the user's experience. For example, suppose the bounds of the search are
486 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on
487 whether revision 50 is good or bad, the next revision to check will be either
488 25 or 75. So, while revision 50 is being checked, the script will download
489 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is
490 known:
492 - If rev 50 is good, the download of rev 25 is cancelled, and the next test
493 is run on rev 75.
495 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
496 is run on rev 25.
499 if not profile:
500 profile = 'profile'
502 context = PathContext(base_url, platform, good_rev, bad_rev,
503 official_builds, is_aura, flash_path)
504 cwd = os.getcwd()
506 print "Downloading list of known revisions..."
507 _GetDownloadPath = lambda rev: os.path.join(cwd,
508 '%s-%s' % (str(rev), context.archive_name))
509 if official_builds:
510 revlist = context.GetOfficialBuildsList()
511 else:
512 revlist = context.GetRevList()
514 # Get a list of revisions to bisect across.
515 if len(revlist) < 2: # Don't have enough builds to bisect.
516 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist
517 raise RuntimeError(msg)
519 # Figure out our bookends and first pivot point; fetch the pivot revision.
520 minrev = 0
521 maxrev = len(revlist) - 1
522 pivot = maxrev / 2
523 rev = revlist[pivot]
524 zipfile = _GetDownloadPath(rev)
525 fetch = DownloadJob(context, 'initial_fetch', rev, zipfile)
526 fetch.Start()
527 fetch.WaitFor()
529 # Binary search time!
530 while fetch and fetch.zipfile and maxrev - minrev > 1:
531 if bad_rev < good_rev:
532 min_str, max_str = "bad", "good"
533 else:
534 min_str, max_str = "good", "bad"
535 print 'Bisecting range [%s (%s), %s (%s)].' % (revlist[minrev], min_str, \
536 revlist[maxrev], max_str)
538 # Pre-fetch next two possible pivots
539 # - down_pivot is the next revision to check if the current revision turns
540 # out to be bad.
541 # - up_pivot is the next revision to check if the current revision turns
542 # out to be good.
543 down_pivot = int((pivot - minrev) / 2) + minrev
544 down_fetch = None
545 if down_pivot != pivot and down_pivot != minrev:
546 down_rev = revlist[down_pivot]
547 down_fetch = DownloadJob(context, 'down_fetch', down_rev,
548 _GetDownloadPath(down_rev))
549 down_fetch.Start()
551 up_pivot = int((maxrev - pivot) / 2) + pivot
552 up_fetch = None
553 if up_pivot != pivot and up_pivot != maxrev:
554 up_rev = revlist[up_pivot]
555 up_fetch = DownloadJob(context, 'up_fetch', up_rev,
556 _GetDownloadPath(up_rev))
557 up_fetch.Start()
559 # Run test on the pivot revision.
560 status = None
561 stdout = None
562 stderr = None
563 try:
564 (status, stdout, stderr) = RunRevision(context,
565 rev,
566 fetch.zipfile,
567 profile,
568 num_runs,
569 command,
570 try_args)
571 except Exception, e:
572 print >> sys.stderr, e
574 # Call the evaluate function to see if the current revision is good or bad.
575 # On that basis, kill one of the background downloads and complete the
576 # other, as described in the comments above.
577 try:
578 if not interactive:
579 if status:
580 answer = 'b'
581 print 'Bad revision: %s' % rev
582 else:
583 answer = 'g'
584 print 'Good revision: %s' % rev
585 else:
586 answer = evaluate(rev, official_builds, status, stdout, stderr)
587 if answer == 'g' and good_rev < bad_rev or \
588 answer == 'b' and bad_rev < good_rev:
589 fetch.Stop()
590 minrev = pivot
591 if down_fetch:
592 down_fetch.Stop() # Kill the download of the older revision.
593 fetch = None
594 if up_fetch:
595 up_fetch.WaitFor()
596 pivot = up_pivot
597 fetch = up_fetch
598 elif answer == 'b' and good_rev < bad_rev or \
599 answer == 'g' and bad_rev < good_rev:
600 fetch.Stop()
601 maxrev = pivot
602 if up_fetch:
603 up_fetch.Stop() # Kill the download of the newer revision.
604 fetch = None
605 if down_fetch:
606 down_fetch.WaitFor()
607 pivot = down_pivot
608 fetch = down_fetch
609 elif answer == 'r':
610 pass # Retry requires no changes.
611 elif answer == 'u':
612 # Nuke the revision from the revlist and choose a new pivot.
613 fetch.Stop()
614 revlist.pop(pivot)
615 maxrev -= 1 # Assumes maxrev >= pivot.
617 if maxrev - minrev > 1:
618 # Alternate between using down_pivot or up_pivot for the new pivot
619 # point, without affecting the range. Do this instead of setting the
620 # pivot to the midpoint of the new range because adjacent revisions
621 # are likely affected by the same issue that caused the (u)nknown
622 # response.
623 if up_fetch and down_fetch:
624 fetch = [up_fetch, down_fetch][len(revlist) % 2]
625 elif up_fetch:
626 fetch = up_fetch
627 else:
628 fetch = down_fetch
629 fetch.WaitFor()
630 if fetch == up_fetch:
631 pivot = up_pivot - 1 # Subtracts 1 because revlist was resized.
632 else:
633 pivot = down_pivot
634 zipfile = fetch.zipfile
636 if down_fetch and fetch != down_fetch:
637 down_fetch.Stop()
638 if up_fetch and fetch != up_fetch:
639 up_fetch.Stop()
640 else:
641 assert False, "Unexpected return value from evaluate(): " + answer
642 except SystemExit:
643 print "Cleaning up..."
644 for f in [_GetDownloadPath(revlist[down_pivot]),
645 _GetDownloadPath(revlist[up_pivot])]:
646 try:
647 os.unlink(f)
648 except OSError:
649 pass
650 sys.exit(0)
652 rev = revlist[pivot]
654 return (revlist[minrev], revlist[maxrev])
657 def GetBlinkDEPSRevisionForChromiumRevision(rev):
658 """Returns the blink revision that was in REVISIONS file at
659 chromium revision |rev|."""
660 # . doesn't match newlines without re.DOTALL, so this is safe.
661 blink_re = re.compile(r'webkit_revision\D*(\d+)')
662 url = urllib.urlopen(DEPS_FILE % rev)
663 m = blink_re.search(url.read())
664 url.close()
665 if m:
666 return int(m.group(1))
667 else:
668 raise Exception('Could not get Blink revision for Chromium rev %d'
669 % rev)
672 def GetBlinkRevisionForChromiumRevision(self, rev):
673 """Returns the blink revision that was in REVISIONS file at
674 chromium revision |rev|."""
675 file_url = "%s/%s%d/REVISIONS" % (self.base_url,
676 self._listing_platform_dir, rev)
677 url = urllib.urlopen(file_url)
678 data = json.loads(url.read())
679 url.close()
680 if 'webkit_revision' in data:
681 return data['webkit_revision']
682 else:
683 raise Exception('Could not get blink revision for cr rev %d' % rev)
685 def FixChromiumRevForBlink(revisions_final, revisions, self, rev):
686 """Returns the chromium revision that has the correct blink revision
687 for blink bisect, DEPS and REVISIONS file might not match since
688 blink snapshots point to tip of tree blink.
689 Note: The revisions_final variable might get modified to include
690 additional revisions."""
692 blink_deps_rev = GetBlinkDEPSRevisionForChromiumRevision(rev)
694 while (GetBlinkRevisionForChromiumRevision(self, rev) > blink_deps_rev):
695 idx = revisions.index(rev)
696 if idx > 0:
697 rev = revisions[idx-1]
698 if rev not in revisions_final:
699 revisions_final.insert(0, rev)
701 revisions_final.sort()
702 return rev
704 def GetChromiumRevision(url):
705 """Returns the chromium revision read from given URL."""
706 try:
707 # Location of the latest build revision number
708 return int(urllib.urlopen(url).read())
709 except Exception, e:
710 print('Could not determine latest revision. This could be bad...')
711 return 999999999
714 def main():
715 usage = ('%prog [options] [-- chromium-options]\n'
716 'Perform binary search on the snapshot builds to find a minimal\n'
717 'range of revisions where a behavior change happened. The\n'
718 'behaviors are described as "good" and "bad".\n'
719 'It is NOT assumed that the behavior of the later revision is\n'
720 'the bad one.\n'
721 '\n'
722 'Revision numbers should use\n'
723 ' Official versions (e.g. 1.0.1000.0) for official builds. (-o)\n'
724 ' SVN revisions (e.g. 123456) for chromium builds, from trunk.\n'
725 ' Use base_trunk_revision from http://omahaproxy.appspot.com/\n'
726 ' for earlier revs.\n'
727 ' Chrome\'s about: build number and omahaproxy branch_revision\n'
728 ' are incorrect, they are from branches.\n'
729 '\n'
730 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
731 parser = optparse.OptionParser(usage=usage)
732 # Strangely, the default help output doesn't include the choice list.
733 choices = ['mac', 'win', 'linux', 'linux64', 'linux-arm']
734 # linux-chromiumos lacks a continuous archive http://crbug.com/78158
735 parser.add_option('-a', '--archive',
736 choices = choices,
737 help = 'The buildbot archive to bisect [%s].' %
738 '|'.join(choices))
739 parser.add_option('-o', action="store_true", dest='official_builds',
740 help = 'Bisect across official ' +
741 'Chrome builds (internal only) instead of ' +
742 'Chromium archives.')
743 parser.add_option('-b', '--bad', type = 'str',
744 help = 'A bad revision to start bisection. ' +
745 'May be earlier or later than the good revision. ' +
746 'Default is HEAD.')
747 parser.add_option('-f', '--flash_path', type = 'str',
748 help = 'Absolute path to a recent Adobe Pepper Flash ' +
749 'binary to be used in this bisection (e.g. ' +
750 'on Windows C:\...\pepflashplayer.dll and on Linux ' +
751 '/opt/google/chrome/PepperFlash/libpepflashplayer.so).')
752 parser.add_option('-g', '--good', type = 'str',
753 help = 'A good revision to start bisection. ' +
754 'May be earlier or later than the bad revision. ' +
755 'Default is 0.')
756 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str',
757 help = 'Profile to use; this will not reset every run. ' +
758 'Defaults to a clean profile.', default = 'profile')
759 parser.add_option('-t', '--times', type = 'int',
760 help = 'Number of times to run each build before asking ' +
761 'if it\'s good or bad. Temporary profiles are reused.',
762 default = 1)
763 parser.add_option('-c', '--command', type = 'str',
764 help = 'Command to execute. %p and %a refer to Chrome ' +
765 'executable and specified extra arguments respectively. ' +
766 'Use %s to specify all extra arguments as one string. ' +
767 'Defaults to "%p %a". Note that any extra paths ' +
768 'specified should be absolute.',
769 default = '%p %a')
770 parser.add_option('-l', '--blink', action='store_true',
771 help = 'Use Blink bisect instead of Chromium. ')
772 parser.add_option('', '--not-interactive', action='store_true',
773 help = 'Use command exit code to tell good/bad revision.',
774 default=False)
775 parser.add_option('--aura',
776 dest='aura',
777 action='store_true',
778 default=False,
779 help='Allow the script to bisect aura builds')
781 (opts, args) = parser.parse_args()
783 if opts.archive is None:
784 print 'Error: missing required parameter: --archive'
785 print
786 parser.print_help()
787 return 1
789 if opts.aura:
790 if opts.archive != 'win' or not opts.official_builds:
791 print 'Error: Aura is supported only on Windows platform '\
792 'and official builds.'
793 return 1
795 if opts.blink:
796 base_url = WEBKIT_BASE_URL
797 else:
798 base_url = CHROMIUM_BASE_URL
800 # Create the context. Initialize 0 for the revisions as they are set below.
801 context = PathContext(base_url, opts.archive, 0, 0,
802 opts.official_builds, opts.aura, None)
803 # Pick a starting point, try to get HEAD for this.
804 if opts.bad:
805 bad_rev = opts.bad
806 else:
807 bad_rev = '999.0.0.0'
808 if not opts.official_builds:
809 bad_rev = GetChromiumRevision(context.GetLastChangeURL())
811 # Find out when we were good.
812 if opts.good:
813 good_rev = opts.good
814 else:
815 good_rev = '0.0.0.0' if opts.official_builds else 0
817 if opts.flash_path:
818 flash_path = opts.flash_path
819 msg = 'Could not find Flash binary at %s' % flash_path
820 assert os.path.exists(flash_path), msg
822 if opts.official_builds:
823 good_rev = LooseVersion(good_rev)
824 bad_rev = LooseVersion(bad_rev)
825 else:
826 good_rev = int(good_rev)
827 bad_rev = int(bad_rev)
829 if opts.times < 1:
830 print('Number of times to run (%d) must be greater than or equal to 1.' %
831 opts.times)
832 parser.print_help()
833 return 1
835 (min_chromium_rev, max_chromium_rev) = Bisect(
836 base_url, opts.archive, opts.official_builds, opts.aura, good_rev,
837 bad_rev, opts.times, opts.command, args, opts.profile, opts.flash_path,
838 not opts.not_interactive)
840 # Get corresponding blink revisions.
841 try:
842 min_blink_rev = GetBlinkRevisionForChromiumRevision(context,
843 min_chromium_rev)
844 max_blink_rev = GetBlinkRevisionForChromiumRevision(context,
845 max_chromium_rev)
846 except Exception, e:
847 # Silently ignore the failure.
848 min_blink_rev, max_blink_rev = 0, 0
850 if opts.blink:
851 # We're done. Let the user know the results in an official manner.
852 if good_rev > bad_rev:
853 print DONE_MESSAGE_GOOD_MAX % (str(min_blink_rev), str(max_blink_rev))
854 else:
855 print DONE_MESSAGE_GOOD_MIN % (str(min_blink_rev), str(max_blink_rev))
857 print 'BLINK CHANGELOG URL:'
858 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev)
860 else:
861 # We're done. Let the user know the results in an official manner.
862 if good_rev > bad_rev:
863 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev),
864 str(max_chromium_rev))
865 else:
866 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev),
867 str(max_chromium_rev))
868 if min_blink_rev != max_blink_rev:
869 print ("NOTE: There is a Blink roll in the range, "
870 "you might also want to do a Blink bisect.")
872 print 'CHANGELOG URL:'
873 if opts.official_builds:
874 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
875 else:
876 print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
878 if __name__ == '__main__':
879 sys.exit(main())