blob: 75237d5b521f73e8c9514cb1836b5cc696ec1254 [file] [log] [blame]
[email protected]cb155a82011-11-29 17:25:341#!/usr/bin/env python
[email protected]5e93cf162012-01-28 02:16:562# Copyright (c) 2012 The Chromium Authors. All rights reserved.
[email protected]67e0bc62009-09-03 22:06:093# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Snapshot Build Bisect Tool
7
[email protected]7ad66a72009-09-04 17:52:338This script bisects a snapshot archive using binary search. It starts at
[email protected]67e0bc62009-09-03 22:06:099a bad revision (it will try to guess HEAD) and asks for a last known-good
10revision. It will then binary search across this revision range by downloading,
11unzipping, and opening Chromium for you. After testing the specific revision,
12it will ask you whether it is good or bad before continuing the search.
[email protected]67e0bc62009-09-03 22:06:0913"""
14
[email protected]4df583c2014-07-31 17:11:5515# The base URL for stored build archives.
16CHROMIUM_BASE_URL = ('http://commondatastorage.googleapis.com'
17 '/chromium-browser-snapshots')
18WEBKIT_BASE_URL = ('http://commondatastorage.googleapis.com'
19 '/chromium-webkit-snapshots')
[email protected]011886692014-08-01 21:00:2120ASAN_BASE_URL = ('http://commondatastorage.googleapis.com'
21 '/chromium-browser-asan')
[email protected]67e0bc62009-09-03 22:06:0922
[email protected]4df583c2014-07-31 17:11:5523# The base URL for official builds.
[email protected]b2905832012-07-19 21:28:4324OFFICIAL_BASE_URL = 'http://master.chrome.corp.google.com/official_builds'
[email protected]d0149c5c2012-05-29 21:12:1125
[email protected]4df583c2014-07-31 17:11:5526# URL template for viewing changelogs between revisions.
27CHANGELOG_URL = ('http://build.chromium.org'
28 '/f/chromium/perf/dashboard/ui/changelog.html'
29 '?url=/trunk/src&range=%d%%3A%d')
[email protected]f6a71a72009-10-08 19:55:3830
[email protected]4df583c2014-07-31 17:11:5531# URL template for viewing changelogs between official versions.
32OFFICIAL_CHANGELOG_URL = ('http://omahaproxy.appspot.com/changelog'
33 '?old_version=%s&new_version=%s')
[email protected]d0149c5c2012-05-29 21:12:1134
[email protected]b2fe7f22011-10-25 22:58:3135# DEPS file URL.
[email protected]fc3702e2013-11-09 04:23:0036DEPS_FILE = 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d'
[email protected]b2fe7f22011-10-25 22:58:3137
[email protected]4df583c2014-07-31 17:11:5538# Blink changelogs URL.
39BLINK_CHANGELOG_URL = ('http://build.chromium.org'
40 '/f/chromium/perf/dashboard/ui/changelog_blink.html'
41 '?url=/trunk&range=%d%%3A%d')
42
43DONE_MESSAGE_GOOD_MIN = ('You are probably looking for a change made after %s ('
44 'known good), but no later than %s (first known bad).')
45DONE_MESSAGE_GOOD_MAX = ('You are probably looking for a change made after %s ('
46 'known bad), but no later than %s (first known good).')
[email protected]05ff3fd2012-04-17 23:24:0647
[email protected]3e7c85322014-06-27 20:27:3648CHROMIUM_GITHASH_TO_SVN_URL = (
49 'https://chromium.googlesource.com/chromium/src/+/%s?format=json')
[email protected]4df583c2014-07-31 17:11:5550
[email protected]3e7c85322014-06-27 20:27:3651BLINK_GITHASH_TO_SVN_URL = (
52 'https://chromium.googlesource.com/chromium/blink/+/%s?format=json')
[email protected]4df583c2014-07-31 17:11:5553
54GITHASH_TO_SVN_URL = {
55 'chromium': CHROMIUM_GITHASH_TO_SVN_URL,
56 'blink': BLINK_GITHASH_TO_SVN_URL,
57}
58
59# Search pattern to be matched in the JSON output from
[email protected]3e7c85322014-06-27 20:27:3660# CHROMIUM_GITHASH_TO_SVN_URL to get the chromium revision (svn revision).
61CHROMIUM_SEARCH_PATTERN = (
62 r'.*git-svn-id: svn://svn.chromium.org/chrome/trunk/src@(\d+) ')
[email protected]4df583c2014-07-31 17:11:5563
[email protected]3e7c85322014-06-27 20:27:3664# Search pattern to be matched in the json output from
65# BLINK_GITHASH_TO_SVN_URL to get the blink revision (svn revision).
66BLINK_SEARCH_PATTERN = (
67 r'.*git-svn-id: svn://svn.chromium.org/blink/trunk@(\d+) ')
[email protected]4df583c2014-07-31 17:11:5568
69SEARCH_PATTERN = {
70 'chromium': CHROMIUM_SEARCH_PATTERN,
71 'blink': BLINK_SEARCH_PATTERN,
72}
[email protected]3e7c85322014-06-27 20:27:3673
[email protected]67e0bc62009-09-03 22:06:0974###############################################################################
75
[email protected]4c6fec6b2013-09-17 17:44:0876import json
[email protected]7ad66a72009-09-04 17:52:3377import optparse
[email protected]67e0bc62009-09-03 22:06:0978import os
79import re
[email protected]61ea90a2013-09-26 10:17:3480import shlex
[email protected]67e0bc62009-09-03 22:06:0981import shutil
[email protected]afe30662011-07-30 01:05:5282import subprocess
[email protected]67e0bc62009-09-03 22:06:0983import sys
[email protected]7ad66a72009-09-04 17:52:3384import tempfile
[email protected]afe30662011-07-30 01:05:5285import threading
[email protected]67e0bc62009-09-03 22:06:0986import urllib
[email protected]d0149c5c2012-05-29 21:12:1187from distutils.version import LooseVersion
[email protected]183706d92011-06-10 13:06:2288from xml.etree import ElementTree
[email protected]bd8dcb92010-03-31 01:05:2489import zipfile
90
[email protected]cb155a82011-11-29 17:25:3491
[email protected]183706d92011-06-10 13:06:2292class PathContext(object):
93 """A PathContext is used to carry the information used to construct URLs and
94 paths when dealing with the storage server and archives."""
[email protected]4c6fec6b2013-09-17 17:44:0895 def __init__(self, base_url, platform, good_revision, bad_revision,
[email protected]011886692014-08-01 21:00:2196 is_official, is_aura, is_asan, use_local_repo, flash_path = None,
[email protected]cdb77062014-07-21 18:07:1597 pdf_path = None):
[email protected]183706d92011-06-10 13:06:2298 super(PathContext, self).__init__()
99 # Store off the input parameters.
[email protected]4c6fec6b2013-09-17 17:44:08100 self.base_url = base_url
[email protected]183706d92011-06-10 13:06:22101 self.platform = platform # What's passed in to the '-a/--archive' option.
102 self.good_revision = good_revision
103 self.bad_revision = bad_revision
[email protected]d0149c5c2012-05-29 21:12:11104 self.is_official = is_official
[email protected]b3b20512013-08-26 18:51:04105 self.is_aura = is_aura
[email protected]011886692014-08-01 21:00:21106 self.is_asan = is_asan
107 self.build_type = 'release'
[email protected]fc3702e2013-11-09 04:23:00108 self.flash_path = flash_path
[email protected]3e7c85322014-06-27 20:27:36109 # Dictionary which stores svn revision number as key and it's
110 # corresponding git hash as value. This data is populated in
111 # _FetchAndParse and used later in GetDownloadURL while downloading
112 # the build.
113 self.githash_svn_dict = {}
[email protected]cdb77062014-07-21 18:07:15114 self.pdf_path = pdf_path
[email protected]183706d92011-06-10 13:06:22115
116 # The name of the ZIP file in a revision directory on the server.
117 self.archive_name = None
118
[email protected]6a7a5d62014-07-09 04:45:50119 # If the script is run from a local Chromium checkout,
120 # "--use-local-repo" option can be used to make the script run faster.
121 # It uses "git svn find-rev <SHA1>" command to convert git hash to svn
122 # revision number.
123 self.use_local_repo = use_local_repo
124
[email protected]183706d92011-06-10 13:06:22125 # Set some internal members:
126 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'.
127 # _archive_extract_dir = Uncompressed directory in the archive_name file.
128 # _binary_name = The name of the executable to run.
[email protected]7aec9e82013-05-09 05:09:23129 if self.platform in ('linux', 'linux64', 'linux-arm'):
[email protected]183706d92011-06-10 13:06:22130 self._binary_name = 'chrome'
[email protected]183706d92011-06-10 13:06:22131 elif self.platform == 'mac':
[email protected]183706d92011-06-10 13:06:22132 self.archive_name = 'chrome-mac.zip'
133 self._archive_extract_dir = 'chrome-mac'
[email protected]183706d92011-06-10 13:06:22134 elif self.platform == 'win':
[email protected]183706d92011-06-10 13:06:22135 self.archive_name = 'chrome-win32.zip'
136 self._archive_extract_dir = 'chrome-win32'
137 self._binary_name = 'chrome.exe'
138 else:
[email protected]afe30662011-07-30 01:05:52139 raise Exception('Invalid platform: %s' % self.platform)
[email protected]183706d92011-06-10 13:06:22140
[email protected]d0149c5c2012-05-29 21:12:11141 if is_official:
142 if self.platform == 'linux':
[email protected]9639b002013-08-30 14:45:52143 self._listing_platform_dir = 'precise32bit/'
144 self.archive_name = 'chrome-precise32bit.zip'
145 self._archive_extract_dir = 'chrome-precise32bit'
[email protected]d0149c5c2012-05-29 21:12:11146 elif self.platform == 'linux64':
[email protected]9639b002013-08-30 14:45:52147 self._listing_platform_dir = 'precise64bit/'
148 self.archive_name = 'chrome-precise64bit.zip'
149 self._archive_extract_dir = 'chrome-precise64bit'
[email protected]d0149c5c2012-05-29 21:12:11150 elif self.platform == 'mac':
151 self._listing_platform_dir = 'mac/'
152 self._binary_name = 'Google Chrome.app/Contents/MacOS/Google Chrome'
153 elif self.platform == 'win':
[email protected]b3b20512013-08-26 18:51:04154 if self.is_aura:
155 self._listing_platform_dir = 'win-aura/'
156 else:
157 self._listing_platform_dir = 'win/'
[email protected]d0149c5c2012-05-29 21:12:11158 else:
[email protected]7aec9e82013-05-09 05:09:23159 if self.platform in ('linux', 'linux64', 'linux-arm'):
[email protected]d0149c5c2012-05-29 21:12:11160 self.archive_name = 'chrome-linux.zip'
161 self._archive_extract_dir = 'chrome-linux'
162 if self.platform == 'linux':
163 self._listing_platform_dir = 'Linux/'
164 elif self.platform == 'linux64':
165 self._listing_platform_dir = 'Linux_x64/'
[email protected]7aec9e82013-05-09 05:09:23166 elif self.platform == 'linux-arm':
167 self._listing_platform_dir = 'Linux_ARM_Cross-Compile/'
[email protected]d0149c5c2012-05-29 21:12:11168 elif self.platform == 'mac':
169 self._listing_platform_dir = 'Mac/'
170 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium'
171 elif self.platform == 'win':
172 self._listing_platform_dir = 'Win/'
173
[email protected]011886692014-08-01 21:00:21174 def GetASANPlatformDir(self):
175 """ASAN builds are in directories like "linux-release", or have filenames
176 like "asan-win32-release-277079.zip". This aligns to our platform names
177 except in the case of Windows where they use "win32" instead of "win"."""
178 if self.platform == 'win':
179 return 'win32'
180 else:
181 return self.platform
182
[email protected]183706d92011-06-10 13:06:22183 def GetListingURL(self, marker=None):
184 """Returns the URL for a directory listing, with an optional marker."""
185 marker_param = ''
186 if marker:
187 marker_param = '&marker=' + str(marker)
[email protected]011886692014-08-01 21:00:21188 if self.is_asan:
189 prefix = '%s-%s' % (self.GetASANPlatformDir(), self.build_type)
190 return self.base_url + '/?delimiter=&prefix=' + prefix + marker_param
191 else:
192 return (self.base_url + '/?delimiter=/&prefix=' +
193 self._listing_platform_dir + marker_param)
[email protected]183706d92011-06-10 13:06:22194
195 def GetDownloadURL(self, revision):
196 """Gets the download URL for a build archive of a specific revision."""
[email protected]011886692014-08-01 21:00:21197 if self.is_asan:
198 return '%s/%s-%s/%s-%d.zip' % (
199 ASAN_BASE_URL, self.GetASANPlatformDir(), self.build_type,
200 self.GetASANBaseName(), revision)
[email protected]d0149c5c2012-05-29 21:12:11201 if self.is_official:
[email protected]4df583c2014-07-31 17:11:55202 return '%s/%s/%s%s' % (
[email protected]d0149c5c2012-05-29 21:12:11203 OFFICIAL_BASE_URL, revision, self._listing_platform_dir,
204 self.archive_name)
205 else:
[email protected]3e7c85322014-06-27 20:27:36206 if str(revision) in self.githash_svn_dict:
207 revision = self.githash_svn_dict[str(revision)]
[email protected]4df583c2014-07-31 17:11:55208 return '%s/%s%s/%s' % (self.base_url, self._listing_platform_dir,
[email protected]4c6fec6b2013-09-17 17:44:08209 revision, self.archive_name)
[email protected]183706d92011-06-10 13:06:22210
211 def GetLastChangeURL(self):
212 """Returns a URL to the LAST_CHANGE file."""
[email protected]4c6fec6b2013-09-17 17:44:08213 return self.base_url + '/' + self._listing_platform_dir + 'LAST_CHANGE'
[email protected]183706d92011-06-10 13:06:22214
[email protected]011886692014-08-01 21:00:21215 def GetASANBaseName(self):
216 """Returns the base name of the ASAN zip file."""
217 if 'linux' in self.platform:
218 return 'asan-symbolized-%s-%s' % (self.GetASANPlatformDir(),
219 self.build_type)
220 else:
221 return 'asan-%s-%s' % (self.GetASANPlatformDir(), self.build_type)
222
223 def GetLaunchPath(self, revision):
[email protected]183706d92011-06-10 13:06:22224 """Returns a relative path (presumably from the archive extraction location)
225 that is used to run the executable."""
[email protected]011886692014-08-01 21:00:21226 if self.is_asan:
227 extract_dir = '%s-%d' % (self.GetASANBaseName(), revision)
228 else:
229 extract_dir = self._archive_extract_dir
230 return os.path.join(extract_dir, self._binary_name)
[email protected]183706d92011-06-10 13:06:22231
[email protected]4df583c2014-07-31 17:11:55232 @staticmethod
233 def IsAuraBuild(build):
234 """Checks whether the given build is an Aura build."""
[email protected]b3b20512013-08-26 18:51:04235 return build.split('.')[3] == '1'
236
[email protected]4df583c2014-07-31 17:11:55237 @staticmethod
[email protected]011886692014-08-01 21:00:21238 def IsOfficialASANBuild(build):
[email protected]4df583c2014-07-31 17:11:55239 """Checks whether the given build is an ASAN build."""
[email protected]b3b20512013-08-26 18:51:04240 return build.split('.')[3] == '2'
241
[email protected]afe30662011-07-30 01:05:52242 def ParseDirectoryIndex(self):
243 """Parses the Google Storage directory listing into a list of revision
[email protected]eadd95d2012-11-02 22:42:09244 numbers."""
[email protected]afe30662011-07-30 01:05:52245
246 def _FetchAndParse(url):
247 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If
248 next-marker is not None, then the listing is a partial listing and another
249 fetch should be performed with next-marker being the marker= GET
250 parameter."""
251 handle = urllib.urlopen(url)
252 document = ElementTree.parse(handle)
253
254 # All nodes in the tree are namespaced. Get the root's tag name to extract
255 # the namespace. Etree does namespaces as |{namespace}tag|.
256 root_tag = document.getroot().tag
257 end_ns_pos = root_tag.find('}')
258 if end_ns_pos == -1:
[email protected]4df583c2014-07-31 17:11:55259 raise Exception('Could not locate end namespace for directory index')
[email protected]afe30662011-07-30 01:05:52260 namespace = root_tag[:end_ns_pos + 1]
261
262 # Find the prefix (_listing_platform_dir) and whether or not the list is
263 # truncated.
264 prefix_len = len(document.find(namespace + 'Prefix').text)
265 next_marker = None
266 is_truncated = document.find(namespace + 'IsTruncated')
267 if is_truncated is not None and is_truncated.text.lower() == 'true':
268 next_marker = document.find(namespace + 'NextMarker').text
[email protected]afe30662011-07-30 01:05:52269 # Get a list of all the revisions.
[email protected]afe30662011-07-30 01:05:52270 revisions = []
[email protected]3e7c85322014-06-27 20:27:36271 githash_svn_dict = {}
[email protected]011886692014-08-01 21:00:21272 if self.is_asan:
273 asan_regex = re.compile(r'.*%s-(\d+)\.zip$' % (self.GetASANBaseName()))
274 # Non ASAN builds are in a <revision> directory. The ASAN builds are
275 # flat
276 all_prefixes = document.findall(namespace + 'Contents/' +
277 namespace + 'Key')
278 for prefix in all_prefixes:
279 m = asan_regex.match(prefix.text)
280 if m:
281 try:
282 revisions.append(int(m.group(1)))
283 except ValueError:
284 pass
285 else:
286 all_prefixes = document.findall(namespace + 'CommonPrefixes/' +
287 namespace + 'Prefix')
288 # The <Prefix> nodes have content of the form of
289 # |_listing_platform_dir/revision/|. Strip off the platform dir and the
290 # trailing slash to just have a number.
291 for prefix in all_prefixes:
292 revnum = prefix.text[prefix_len:-1]
293 try:
294 if not revnum.isdigit():
295 git_hash = revnum
296 revnum = self.GetSVNRevisionFromGitHash(git_hash)
297 githash_svn_dict[revnum] = git_hash
298 if revnum is not None:
299 revnum = int(revnum)
300 revisions.append(revnum)
301 except ValueError:
302 pass
[email protected]3e7c85322014-06-27 20:27:36303 return (revisions, next_marker, githash_svn_dict)
[email protected]9639b002013-08-30 14:45:52304
[email protected]afe30662011-07-30 01:05:52305 # Fetch the first list of revisions.
[email protected]4df583c2014-07-31 17:11:55306 (revisions, next_marker, self.githash_svn_dict) = _FetchAndParse(
307 self.GetListingURL())
[email protected]afe30662011-07-30 01:05:52308 # If the result list was truncated, refetch with the next marker. Do this
309 # until an entire directory listing is done.
310 while next_marker:
311 next_url = self.GetListingURL(next_marker)
[email protected]3e7c85322014-06-27 20:27:36312 (new_revisions, next_marker, new_dict) = _FetchAndParse(next_url)
[email protected]afe30662011-07-30 01:05:52313 revisions.extend(new_revisions)
[email protected]3e7c85322014-06-27 20:27:36314 self.githash_svn_dict.update(new_dict)
[email protected]afe30662011-07-30 01:05:52315 return revisions
316
[email protected]6a7a5d62014-07-09 04:45:50317 def _GetSVNRevisionFromGitHashWithoutGitCheckout(self, git_sha1, depot):
[email protected]3e7c85322014-06-27 20:27:36318 json_url = GITHASH_TO_SVN_URL[depot] % git_sha1
[email protected]2e0f2672014-08-13 20:32:58319 response = urllib.urlopen(json_url)
320 if response.getcode() == 200:
321 try:
322 data = json.loads(response.read()[4:])
323 except ValueError:
324 print 'ValueError for JSON URL: %s' % json_url
325 raise ValueError
326 else:
327 raise ValueError
[email protected]3e7c85322014-06-27 20:27:36328 if 'message' in data:
329 message = data['message'].split('\n')
330 message = [line for line in message if line.strip()]
331 search_pattern = re.compile(SEARCH_PATTERN[depot])
332 result = search_pattern.search(message[len(message)-1])
333 if result:
334 return result.group(1)
335 print 'Failed to get svn revision number for %s' % git_sha1
[email protected]1f99f4d2014-07-23 16:44:14336 raise ValueError
[email protected]3e7c85322014-06-27 20:27:36337
[email protected]6a7a5d62014-07-09 04:45:50338 def _GetSVNRevisionFromGitHashFromGitCheckout(self, git_sha1, depot):
339 def _RunGit(command, path):
340 command = ['git'] + command
341 if path:
342 original_path = os.getcwd()
343 os.chdir(path)
344 shell = sys.platform.startswith('win')
345 proc = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE,
346 stderr=subprocess.PIPE)
347 (output, _) = proc.communicate()
348
349 if path:
350 os.chdir(original_path)
351 return (output, proc.returncode)
352
353 path = None
354 if depot == 'blink':
355 path = os.path.join(os.getcwd(), 'third_party', 'WebKit')
356 if os.path.basename(os.getcwd()) == 'src':
357 command = ['svn', 'find-rev', git_sha1]
358 (git_output, return_code) = _RunGit(command, path)
359 if not return_code:
360 return git_output.strip('\n')
[email protected]1f99f4d2014-07-23 16:44:14361 raise ValueError
[email protected]6a7a5d62014-07-09 04:45:50362 else:
363 print ('Script should be run from src folder. ' +
364 'Eg: python tools/bisect-builds.py -g 280588 -b 280590' +
365 '--archive linux64 --use-local-repo')
366 sys.exit(1)
367
368 def GetSVNRevisionFromGitHash(self, git_sha1, depot='chromium'):
369 if not self.use_local_repo:
370 return self._GetSVNRevisionFromGitHashWithoutGitCheckout(git_sha1, depot)
371 else:
372 return self._GetSVNRevisionFromGitHashFromGitCheckout(git_sha1, depot)
373
[email protected]afe30662011-07-30 01:05:52374 def GetRevList(self):
375 """Gets the list of revision numbers between self.good_revision and
376 self.bad_revision."""
377 # Download the revlist and filter for just the range between good and bad.
[email protected]eadd95d2012-11-02 22:42:09378 minrev = min(self.good_revision, self.bad_revision)
379 maxrev = max(self.good_revision, self.bad_revision)
[email protected]37ed3172013-09-24 23:49:30380 revlist_all = map(int, self.ParseDirectoryIndex())
381
382 revlist = [x for x in revlist_all if x >= int(minrev) and x <= int(maxrev)]
[email protected]afe30662011-07-30 01:05:52383 revlist.sort()
[email protected]37ed3172013-09-24 23:49:30384
385 # Set good and bad revisions to be legit revisions.
386 if revlist:
387 if self.good_revision < self.bad_revision:
388 self.good_revision = revlist[0]
389 self.bad_revision = revlist[-1]
390 else:
391 self.bad_revision = revlist[0]
392 self.good_revision = revlist[-1]
393
394 # Fix chromium rev so that the deps blink revision matches REVISIONS file.
395 if self.base_url == WEBKIT_BASE_URL:
396 revlist_all.sort()
397 self.good_revision = FixChromiumRevForBlink(revlist,
398 revlist_all,
399 self,
400 self.good_revision)
401 self.bad_revision = FixChromiumRevForBlink(revlist,
402 revlist_all,
403 self,
404 self.bad_revision)
[email protected]afe30662011-07-30 01:05:52405 return revlist
406
[email protected]d0149c5c2012-05-29 21:12:11407 def GetOfficialBuildsList(self):
408 """Gets the list of official build numbers between self.good_revision and
409 self.bad_revision."""
410 # Download the revlist and filter for just the range between good and bad.
[email protected]eadd95d2012-11-02 22:42:09411 minrev = min(self.good_revision, self.bad_revision)
412 maxrev = max(self.good_revision, self.bad_revision)
[email protected]d0149c5c2012-05-29 21:12:11413 handle = urllib.urlopen(OFFICIAL_BASE_URL)
414 dirindex = handle.read()
415 handle.close()
416 build_numbers = re.findall(r'<a href="([0-9][0-9].*)/">', dirindex)
417 final_list = []
[email protected]d0149c5c2012-05-29 21:12:11418 i = 0
[email protected]d0149c5c2012-05-29 21:12:11419 parsed_build_numbers = [LooseVersion(x) for x in build_numbers]
420 for build_number in sorted(parsed_build_numbers):
[email protected]4df583c2014-07-31 17:11:55421 path = (OFFICIAL_BASE_URL + '/' + str(build_number) + '/' +
422 self._listing_platform_dir + self.archive_name)
[email protected]d0149c5c2012-05-29 21:12:11423 i = i + 1
424 try:
425 connection = urllib.urlopen(path)
426 connection.close()
[email protected]801fb652012-07-20 20:13:50427 if build_number > maxrev:
428 break
429 if build_number >= minrev:
[email protected]b3b20512013-08-26 18:51:04430 # If we are bisecting Aura, we want to include only builds which
431 # ends with ".1".
432 if self.is_aura:
433 if self.IsAuraBuild(str(build_number)):
434 final_list.append(str(build_number))
435 # If we are bisecting only official builds (without --aura),
436 # we can not include builds which ends with '.1' or '.2' since
437 # they have different folder hierarchy inside.
438 elif (not self.IsAuraBuild(str(build_number)) and
[email protected]011886692014-08-01 21:00:21439 not self.IsOfficialASANBuild(str(build_number))):
[email protected]b3b20512013-08-26 18:51:04440 final_list.append(str(build_number))
[email protected]4df583c2014-07-31 17:11:55441 except urllib.HTTPError:
[email protected]d0149c5c2012-05-29 21:12:11442 pass
[email protected]801fb652012-07-20 20:13:50443 return final_list
[email protected]bd8dcb92010-03-31 01:05:24444
[email protected]fc3702e2013-11-09 04:23:00445def UnzipFilenameToDir(filename, directory):
446 """Unzip |filename| to |directory|."""
[email protected]afe30662011-07-30 01:05:52447 cwd = os.getcwd()
448 if not os.path.isabs(filename):
449 filename = os.path.join(cwd, filename)
[email protected]bd8dcb92010-03-31 01:05:24450 zf = zipfile.ZipFile(filename)
451 # Make base.
[email protected]fc3702e2013-11-09 04:23:00452 if not os.path.isdir(directory):
453 os.mkdir(directory)
454 os.chdir(directory)
[email protected]e29c08c2012-09-17 20:50:50455 # Extract files.
456 for info in zf.infolist():
457 name = info.filename
458 if name.endswith('/'): # dir
459 if not os.path.isdir(name):
460 os.makedirs(name)
461 else: # file
[email protected]fc3702e2013-11-09 04:23:00462 directory = os.path.dirname(name)
463 if not os.path.isdir(directory):
464 os.makedirs(directory)
[email protected]e29c08c2012-09-17 20:50:50465 out = open(name, 'wb')
466 out.write(zf.read(name))
467 out.close()
468 # Set permissions. Permission info in external_attr is shifted 16 bits.
469 os.chmod(name, info.external_attr >> 16L)
470 os.chdir(cwd)
[email protected]bd8dcb92010-03-31 01:05:24471
[email protected]67e0bc62009-09-03 22:06:09472
[email protected]468a9772011-08-09 18:42:00473def FetchRevision(context, rev, filename, quit_event=None, progress_event=None):
[email protected]afe30662011-07-30 01:05:52474 """Downloads and unzips revision |rev|.
475 @param context A PathContext instance.
476 @param rev The Chromium revision number/tag to download.
477 @param filename The destination for the downloaded file.
478 @param quit_event A threading.Event which will be set by the master thread to
479 indicate that the download should be aborted.
[email protected]468a9772011-08-09 18:42:00480 @param progress_event A threading.Event which will be set by the master thread
481 to indicate that the progress of the download should be
482 displayed.
[email protected]afe30662011-07-30 01:05:52483 """
484 def ReportHook(blocknum, blocksize, totalsize):
[email protected]946be752011-10-25 23:34:21485 if quit_event and quit_event.isSet():
[email protected]4df583c2014-07-31 17:11:55486 raise RuntimeError('Aborting download of revision %s' % str(rev))
[email protected]946be752011-10-25 23:34:21487 if progress_event and progress_event.isSet():
[email protected]468a9772011-08-09 18:42:00488 size = blocknum * blocksize
489 if totalsize == -1: # Total size not known.
[email protected]4df583c2014-07-31 17:11:55490 progress = 'Received %d bytes' % size
[email protected]468a9772011-08-09 18:42:00491 else:
492 size = min(totalsize, size)
[email protected]4df583c2014-07-31 17:11:55493 progress = 'Received %d of %d bytes, %.2f%%' % (
[email protected]468a9772011-08-09 18:42:00494 size, totalsize, 100.0 * size / totalsize)
495 # Send a \r to let all progress messages use just one line of output.
[email protected]4df583c2014-07-31 17:11:55496 sys.stdout.write('\r' + progress)
[email protected]468a9772011-08-09 18:42:00497 sys.stdout.flush()
[email protected]7ad66a72009-09-04 17:52:33498
[email protected]afe30662011-07-30 01:05:52499 download_url = context.GetDownloadURL(rev)
500 try:
501 urllib.urlretrieve(download_url, filename, ReportHook)
[email protected]946be752011-10-25 23:34:21502 if progress_event and progress_event.isSet():
[email protected]ecaba01e62011-10-26 05:33:28503 print
[email protected]4df583c2014-07-31 17:11:55504 except RuntimeError:
[email protected]afe30662011-07-30 01:05:52505 pass
[email protected]7ad66a72009-09-04 17:52:33506
[email protected]7ad66a72009-09-04 17:52:33507
[email protected]4df583c2014-07-31 17:11:55508def RunRevision(context, revision, zip_file, profile, num_runs, command, args):
[email protected]afe30662011-07-30 01:05:52509 """Given a zipped revision, unzip it and run the test."""
[email protected]4df583c2014-07-31 17:11:55510 print 'Trying revision %s...' % str(revision)
[email protected]3ff00b72011-07-20 21:34:47511
[email protected]afe30662011-07-30 01:05:52512 # Create a temp directory and unzip the revision into it.
[email protected]7ad66a72009-09-04 17:52:33513 cwd = os.getcwd()
514 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
[email protected]4df583c2014-07-31 17:11:55515 UnzipFilenameToDir(zip_file, tempdir)
[email protected]7ad66a72009-09-04 17:52:33516 os.chdir(tempdir)
[email protected]67e0bc62009-09-03 22:06:09517
[email protected]5e93cf162012-01-28 02:16:56518 # Run the build as many times as specified.
[email protected]4646a752013-07-19 22:14:34519 testargs = ['--user-data-dir=%s' % profile] + args
[email protected]d0149c5c2012-05-29 21:12:11520 # The sandbox must be run as root on Official Chrome, so bypass it.
[email protected]cdb77062014-07-21 18:07:15521 if ((context.is_official or context.flash_path or context.pdf_path) and
[email protected]fc3702e2013-11-09 04:23:00522 context.platform.startswith('linux')):
[email protected]d0149c5c2012-05-29 21:12:11523 testargs.append('--no-sandbox')
[email protected]fc3702e2013-11-09 04:23:00524 if context.flash_path:
525 testargs.append('--ppapi-flash-path=%s' % context.flash_path)
526 # We have to pass a large enough Flash version, which currently needs not
527 # be correct. Instead of requiring the user of the script to figure out and
528 # pass the correct version we just spoof it.
529 testargs.append('--ppapi-flash-version=99.9.999.999')
[email protected]d0149c5c2012-05-29 21:12:11530
[email protected]cdb77062014-07-21 18:07:15531 # TODO(vitalybuka): Remove in the future. See crbug.com/395687.
532 if context.pdf_path:
[email protected]011886692014-08-01 21:00:21533 shutil.copy(context.pdf_path,
534 os.path.dirname(context.GetLaunchPath(revision)))
[email protected]cdb77062014-07-21 18:07:15535 testargs.append('--enable-print-preview')
536
[email protected]4646a752013-07-19 22:14:34537 runcommand = []
[email protected]61ea90a2013-09-26 10:17:34538 for token in shlex.split(command):
[email protected]4df583c2014-07-31 17:11:55539 if token == '%a':
[email protected]4646a752013-07-19 22:14:34540 runcommand.extend(testargs)
541 else:
[email protected]4df583c2014-07-31 17:11:55542 runcommand.append(
[email protected]011886692014-08-01 21:00:21543 token.replace('%p', os.path.abspath(context.GetLaunchPath(revision))).
544 replace('%s', ' '.join(testargs)))
[email protected]4646a752013-07-19 22:14:34545
[email protected]d59c8712014-02-11 21:04:57546 results = []
[email protected]4df583c2014-07-31 17:11:55547 for _ in range(num_runs):
[email protected]4646a752013-07-19 22:14:34548 subproc = subprocess.Popen(runcommand,
[email protected]5e93cf162012-01-28 02:16:56549 bufsize=-1,
550 stdout=subprocess.PIPE,
551 stderr=subprocess.PIPE)
552 (stdout, stderr) = subproc.communicate()
[email protected]d59c8712014-02-11 21:04:57553 results.append((subproc.returncode, stdout, stderr))
[email protected]7ad66a72009-09-04 17:52:33554
555 os.chdir(cwd)
[email protected]7ad66a72009-09-04 17:52:33556 try:
557 shutil.rmtree(tempdir, True)
[email protected]4df583c2014-07-31 17:11:55558 except Exception:
[email protected]7ad66a72009-09-04 17:52:33559 pass
[email protected]67e0bc62009-09-03 22:06:09560
[email protected]d59c8712014-02-11 21:04:57561 for (returncode, stdout, stderr) in results:
562 if returncode:
563 return (returncode, stdout, stderr)
564 return results[0]
[email protected]79f14742010-03-10 01:01:57565
[email protected]cb155a82011-11-29 17:25:34566
[email protected]4df583c2014-07-31 17:11:55567# The arguments official_builds, status, stdout and stderr are unused.
568# They are present here because this function is passed to Bisect which then
569# calls it with 5 arguments.
570# pylint: disable=W0613
[email protected]d0149c5c2012-05-29 21:12:11571def AskIsGoodBuild(rev, official_builds, status, stdout, stderr):
[email protected]4df583c2014-07-31 17:11:55572 """Asks the user whether build |rev| is good or bad."""
[email protected]79f14742010-03-10 01:01:57573 # Loop until we get a response that we can parse.
[email protected]67e0bc62009-09-03 22:06:09574 while True:
[email protected]4df583c2014-07-31 17:11:55575 response = raw_input('Revision %s is '
[email protected]1d4a06242013-08-20 22:53:12576 '[(g)ood/(b)ad/(r)etry/(u)nknown/(q)uit]: ' %
[email protected]53bb6342012-06-01 04:11:00577 str(rev))
[email protected]1d4a06242013-08-20 22:53:12578 if response and response in ('g', 'b', 'r', 'u'):
[email protected]53bb6342012-06-01 04:11:00579 return response
[email protected]afe30662011-07-30 01:05:52580 if response and response == 'q':
581 raise SystemExit()
[email protected]67e0bc62009-09-03 22:06:09582
[email protected]cb155a82011-11-29 17:25:34583
[email protected]011886692014-08-01 21:00:21584def IsGoodASANBuild(rev, official_builds, status, stdout, stderr):
585 """Determine if an ASAN build |rev| is good or bad
586
587 Will examine stderr looking for the error message emitted by ASAN. If not
588 found then will fallback to asking the user."""
589 if stderr:
590 bad_count = 0
591 for line in stderr.splitlines():
592 print line
593 if line.find('ERROR: AddressSanitizer:') != -1:
594 bad_count += 1
595 if bad_count > 0:
596 print 'Revision %d determined to be bad.' % rev
597 return 'b'
598 return AskIsGoodBuild(rev, official_builds, status, stdout, stderr)
599
[email protected]53bb6342012-06-01 04:11:00600class DownloadJob(object):
601 """DownloadJob represents a task to download a given Chromium revision."""
[email protected]4df583c2014-07-31 17:11:55602
603 def __init__(self, context, name, rev, zip_file):
[email protected]53bb6342012-06-01 04:11:00604 super(DownloadJob, self).__init__()
605 # Store off the input parameters.
606 self.context = context
607 self.name = name
608 self.rev = rev
[email protected]4df583c2014-07-31 17:11:55609 self.zip_file = zip_file
[email protected]53bb6342012-06-01 04:11:00610 self.quit_event = threading.Event()
611 self.progress_event = threading.Event()
[email protected]4df583c2014-07-31 17:11:55612 self.thread = None
[email protected]53bb6342012-06-01 04:11:00613
614 def Start(self):
615 """Starts the download."""
616 fetchargs = (self.context,
617 self.rev,
[email protected]4df583c2014-07-31 17:11:55618 self.zip_file,
[email protected]53bb6342012-06-01 04:11:00619 self.quit_event,
620 self.progress_event)
621 self.thread = threading.Thread(target=FetchRevision,
622 name=self.name,
623 args=fetchargs)
624 self.thread.start()
625
626 def Stop(self):
627 """Stops the download which must have been started previously."""
[email protected]4df583c2014-07-31 17:11:55628 assert self.thread, 'DownloadJob must be started before Stop is called.'
[email protected]53bb6342012-06-01 04:11:00629 self.quit_event.set()
630 self.thread.join()
[email protected]4df583c2014-07-31 17:11:55631 os.unlink(self.zip_file)
[email protected]53bb6342012-06-01 04:11:00632
633 def WaitFor(self):
634 """Prints a message and waits for the download to complete. The download
635 must have been started previously."""
[email protected]4df583c2014-07-31 17:11:55636 assert self.thread, 'DownloadJob must be started before WaitFor is called.'
637 print 'Downloading revision %s...' % str(self.rev)
[email protected]53bb6342012-06-01 04:11:00638 self.progress_event.set() # Display progress of download.
639 self.thread.join()
640
641
[email protected]2e0f2672014-08-13 20:32:58642def Bisect(context,
[email protected]5e93cf162012-01-28 02:16:56643 num_runs=1,
[email protected]4df583c2014-07-31 17:11:55644 command='%p %a',
[email protected]60ac66e32011-07-18 16:08:25645 try_args=(),
[email protected]afe30662011-07-30 01:05:52646 profile=None,
[email protected]d59c8712014-02-11 21:04:57647 interactive=True,
[email protected]53bb6342012-06-01 04:11:00648 evaluate=AskIsGoodBuild):
[email protected]afe30662011-07-30 01:05:52649 """Given known good and known bad revisions, run a binary search on all
650 archived revisions to determine the last known good revision.
[email protected]60ac66e32011-07-18 16:08:25651
[email protected]2e0f2672014-08-13 20:32:58652 @param context PathContext object initialized with user provided parameters.
[email protected]5e93cf162012-01-28 02:16:56653 @param num_runs Number of times to run each build for asking good/bad.
[email protected]afe30662011-07-30 01:05:52654 @param try_args A tuple of arguments to pass to the test application.
655 @param profile The name of the user profile to run with.
[email protected]d59c8712014-02-11 21:04:57656 @param interactive If it is false, use command exit code for good or bad
657 judgment of the argument build.
[email protected]53bb6342012-06-01 04:11:00658 @param evaluate A function which returns 'g' if the argument build is good,
659 'b' if it's bad or 'u' if unknown.
[email protected]afe30662011-07-30 01:05:52660
661 Threading is used to fetch Chromium revisions in the background, speeding up
662 the user's experience. For example, suppose the bounds of the search are
663 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on
664 whether revision 50 is good or bad, the next revision to check will be either
665 25 or 75. So, while revision 50 is being checked, the script will download
666 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is
667 known:
668
669 - If rev 50 is good, the download of rev 25 is cancelled, and the next test
670 is run on rev 75.
671
672 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
673 is run on rev 25.
[email protected]60ac66e32011-07-18 16:08:25674 """
675
[email protected]afe30662011-07-30 01:05:52676 if not profile:
677 profile = 'profile'
678
[email protected]2e0f2672014-08-13 20:32:58679 good_rev = context.good_revision
680 bad_rev = context.bad_revision
[email protected]afe30662011-07-30 01:05:52681 cwd = os.getcwd()
682
[email protected]28a3c122014-08-09 11:04:51683 print 'Downloading list of known revisions...',
[email protected]2e0f2672014-08-13 20:32:58684 if not context.use_local_repo:
[email protected]28a3c122014-08-09 11:04:51685 print '(use --use-local-repo for speed if you have a local checkout)'
686 else:
687 print
[email protected]d0149c5c2012-05-29 21:12:11688 _GetDownloadPath = lambda rev: os.path.join(cwd,
689 '%s-%s' % (str(rev), context.archive_name))
[email protected]2e0f2672014-08-13 20:32:58690 if context.is_official:
[email protected]d0149c5c2012-05-29 21:12:11691 revlist = context.GetOfficialBuildsList()
692 else:
693 revlist = context.GetRevList()
[email protected]afe30662011-07-30 01:05:52694
695 # Get a list of revisions to bisect across.
696 if len(revlist) < 2: # Don't have enough builds to bisect.
697 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist
698 raise RuntimeError(msg)
699
700 # Figure out our bookends and first pivot point; fetch the pivot revision.
[email protected]eadd95d2012-11-02 22:42:09701 minrev = 0
702 maxrev = len(revlist) - 1
703 pivot = maxrev / 2
[email protected]afe30662011-07-30 01:05:52704 rev = revlist[pivot]
[email protected]4df583c2014-07-31 17:11:55705 zip_file = _GetDownloadPath(rev)
706 fetch = DownloadJob(context, 'initial_fetch', rev, zip_file)
[email protected]eadd95d2012-11-02 22:42:09707 fetch.Start()
708 fetch.WaitFor()
[email protected]60ac66e32011-07-18 16:08:25709
710 # Binary search time!
[email protected]4df583c2014-07-31 17:11:55711 while fetch and fetch.zip_file and maxrev - minrev > 1:
[email protected]eadd95d2012-11-02 22:42:09712 if bad_rev < good_rev:
[email protected]4df583c2014-07-31 17:11:55713 min_str, max_str = 'bad', 'good'
[email protected]eadd95d2012-11-02 22:42:09714 else:
[email protected]4df583c2014-07-31 17:11:55715 min_str, max_str = 'good', 'bad'
716 print 'Bisecting range [%s (%s), %s (%s)].' % (revlist[minrev], min_str,
[email protected]eadd95d2012-11-02 22:42:09717 revlist[maxrev], max_str)
718
[email protected]afe30662011-07-30 01:05:52719 # Pre-fetch next two possible pivots
720 # - down_pivot is the next revision to check if the current revision turns
721 # out to be bad.
722 # - up_pivot is the next revision to check if the current revision turns
723 # out to be good.
[email protected]eadd95d2012-11-02 22:42:09724 down_pivot = int((pivot - minrev) / 2) + minrev
[email protected]53bb6342012-06-01 04:11:00725 down_fetch = None
[email protected]eadd95d2012-11-02 22:42:09726 if down_pivot != pivot and down_pivot != minrev:
[email protected]afe30662011-07-30 01:05:52727 down_rev = revlist[down_pivot]
[email protected]53bb6342012-06-01 04:11:00728 down_fetch = DownloadJob(context, 'down_fetch', down_rev,
729 _GetDownloadPath(down_rev))
730 down_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25731
[email protected]eadd95d2012-11-02 22:42:09732 up_pivot = int((maxrev - pivot) / 2) + pivot
[email protected]53bb6342012-06-01 04:11:00733 up_fetch = None
[email protected]eadd95d2012-11-02 22:42:09734 if up_pivot != pivot and up_pivot != maxrev:
[email protected]afe30662011-07-30 01:05:52735 up_rev = revlist[up_pivot]
[email protected]53bb6342012-06-01 04:11:00736 up_fetch = DownloadJob(context, 'up_fetch', up_rev,
737 _GetDownloadPath(up_rev))
738 up_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25739
[email protected]afe30662011-07-30 01:05:52740 # Run test on the pivot revision.
[email protected]e29c08c2012-09-17 20:50:50741 status = None
742 stdout = None
743 stderr = None
744 try:
745 (status, stdout, stderr) = RunRevision(context,
746 rev,
[email protected]4df583c2014-07-31 17:11:55747 fetch.zip_file,
[email protected]e29c08c2012-09-17 20:50:50748 profile,
749 num_runs,
[email protected]4646a752013-07-19 22:14:34750 command,
[email protected]e29c08c2012-09-17 20:50:50751 try_args)
752 except Exception, e:
[email protected]fc3702e2013-11-09 04:23:00753 print >> sys.stderr, e
[email protected]60ac66e32011-07-18 16:08:25754
[email protected]53bb6342012-06-01 04:11:00755 # Call the evaluate function to see if the current revision is good or bad.
[email protected]afe30662011-07-30 01:05:52756 # On that basis, kill one of the background downloads and complete the
757 # other, as described in the comments above.
758 try:
[email protected]d59c8712014-02-11 21:04:57759 if not interactive:
760 if status:
761 answer = 'b'
762 print 'Bad revision: %s' % rev
763 else:
764 answer = 'g'
765 print 'Good revision: %s' % rev
766 else:
[email protected]2e0f2672014-08-13 20:32:58767 answer = evaluate(rev, context.is_official, status, stdout, stderr)
[email protected]4df583c2014-07-31 17:11:55768 if ((answer == 'g' and good_rev < bad_rev)
769 or (answer == 'b' and bad_rev < good_rev)):
[email protected]1d4a06242013-08-20 22:53:12770 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09771 minrev = pivot
[email protected]53bb6342012-06-01 04:11:00772 if down_fetch:
773 down_fetch.Stop() # Kill the download of the older revision.
[email protected]1d4a06242013-08-20 22:53:12774 fetch = None
[email protected]53bb6342012-06-01 04:11:00775 if up_fetch:
776 up_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52777 pivot = up_pivot
[email protected]eadd95d2012-11-02 22:42:09778 fetch = up_fetch
[email protected]4df583c2014-07-31 17:11:55779 elif ((answer == 'b' and good_rev < bad_rev)
780 or (answer == 'g' and bad_rev < good_rev)):
[email protected]1d4a06242013-08-20 22:53:12781 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09782 maxrev = pivot
[email protected]53bb6342012-06-01 04:11:00783 if up_fetch:
784 up_fetch.Stop() # Kill the download of the newer revision.
[email protected]1d4a06242013-08-20 22:53:12785 fetch = None
[email protected]53bb6342012-06-01 04:11:00786 if down_fetch:
787 down_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52788 pivot = down_pivot
[email protected]eadd95d2012-11-02 22:42:09789 fetch = down_fetch
[email protected]1d4a06242013-08-20 22:53:12790 elif answer == 'r':
791 pass # Retry requires no changes.
[email protected]53bb6342012-06-01 04:11:00792 elif answer == 'u':
793 # Nuke the revision from the revlist and choose a new pivot.
[email protected]1d4a06242013-08-20 22:53:12794 fetch.Stop()
[email protected]53bb6342012-06-01 04:11:00795 revlist.pop(pivot)
[email protected]eadd95d2012-11-02 22:42:09796 maxrev -= 1 # Assumes maxrev >= pivot.
[email protected]53bb6342012-06-01 04:11:00797
[email protected]eadd95d2012-11-02 22:42:09798 if maxrev - minrev > 1:
[email protected]53bb6342012-06-01 04:11:00799 # Alternate between using down_pivot or up_pivot for the new pivot
800 # point, without affecting the range. Do this instead of setting the
801 # pivot to the midpoint of the new range because adjacent revisions
802 # are likely affected by the same issue that caused the (u)nknown
803 # response.
804 if up_fetch and down_fetch:
805 fetch = [up_fetch, down_fetch][len(revlist) % 2]
806 elif up_fetch:
807 fetch = up_fetch
808 else:
809 fetch = down_fetch
810 fetch.WaitFor()
811 if fetch == up_fetch:
812 pivot = up_pivot - 1 # Subtracts 1 because revlist was resized.
813 else:
814 pivot = down_pivot
[email protected]4df583c2014-07-31 17:11:55815 zip_file = fetch.zip_file
[email protected]53bb6342012-06-01 04:11:00816
817 if down_fetch and fetch != down_fetch:
818 down_fetch.Stop()
819 if up_fetch and fetch != up_fetch:
820 up_fetch.Stop()
821 else:
[email protected]4df583c2014-07-31 17:11:55822 assert False, 'Unexpected return value from evaluate(): ' + answer
[email protected]afe30662011-07-30 01:05:52823 except SystemExit:
[email protected]4df583c2014-07-31 17:11:55824 print 'Cleaning up...'
[email protected]5e93cf162012-01-28 02:16:56825 for f in [_GetDownloadPath(revlist[down_pivot]),
826 _GetDownloadPath(revlist[up_pivot])]:
[email protected]afe30662011-07-30 01:05:52827 try:
828 os.unlink(f)
829 except OSError:
830 pass
831 sys.exit(0)
832
833 rev = revlist[pivot]
834
[email protected]2e0f2672014-08-13 20:32:58835 return (revlist[minrev], revlist[maxrev], context)
[email protected]60ac66e32011-07-18 16:08:25836
837
[email protected]37ed3172013-09-24 23:49:30838def GetBlinkDEPSRevisionForChromiumRevision(rev):
[email protected]4c6fec6b2013-09-17 17:44:08839 """Returns the blink revision that was in REVISIONS file at
[email protected]b2fe7f22011-10-25 22:58:31840 chromium revision |rev|."""
841 # . doesn't match newlines without re.DOTALL, so this is safe.
[email protected]37ed3172013-09-24 23:49:30842 blink_re = re.compile(r'webkit_revision\D*(\d+)')
843 url = urllib.urlopen(DEPS_FILE % rev)
844 m = blink_re.search(url.read())
845 url.close()
846 if m:
847 return int(m.group(1))
848 else:
[email protected]4df583c2014-07-31 17:11:55849 raise Exception('Could not get Blink revision for Chromium rev %d' % rev)
[email protected]37ed3172013-09-24 23:49:30850
851
[email protected]2e0f2672014-08-13 20:32:58852def GetBlinkRevisionForChromiumRevision(context, rev):
[email protected]37ed3172013-09-24 23:49:30853 """Returns the blink revision that was in REVISIONS file at
854 chromium revision |rev|."""
[email protected]3e7c85322014-06-27 20:27:36855 def _IsRevisionNumber(revision):
856 if isinstance(revision, int):
857 return True
858 else:
859 return revision.isdigit()
[email protected]2e0f2672014-08-13 20:32:58860 if str(rev) in context.githash_svn_dict:
861 rev = context.githash_svn_dict[str(rev)]
862 file_url = '%s/%s%s/REVISIONS' % (context.base_url,
863 context._listing_platform_dir, rev)
[email protected]4c6fec6b2013-09-17 17:44:08864 url = urllib.urlopen(file_url)
[email protected]2e0f2672014-08-13 20:32:58865 if url.getcode() == 200:
866 try:
867 data = json.loads(url.read())
868 except ValueError:
869 print 'ValueError for JSON URL: %s' % file_url
870 raise ValueError
871 else:
872 raise ValueError
[email protected]b2fe7f22011-10-25 22:58:31873 url.close()
[email protected]4c6fec6b2013-09-17 17:44:08874 if 'webkit_revision' in data:
[email protected]3e7c85322014-06-27 20:27:36875 blink_rev = data['webkit_revision']
876 if not _IsRevisionNumber(blink_rev):
[email protected]2e0f2672014-08-13 20:32:58877 blink_rev = int(context.GetSVNRevisionFromGitHash(blink_rev, 'blink'))
[email protected]3e7c85322014-06-27 20:27:36878 return blink_rev
[email protected]b2fe7f22011-10-25 22:58:31879 else:
[email protected]ff50d1c2013-04-17 18:49:36880 raise Exception('Could not get blink revision for cr rev %d' % rev)
[email protected]b2fe7f22011-10-25 22:58:31881
[email protected]4df583c2014-07-31 17:11:55882
[email protected]37ed3172013-09-24 23:49:30883def FixChromiumRevForBlink(revisions_final, revisions, self, rev):
884 """Returns the chromium revision that has the correct blink revision
885 for blink bisect, DEPS and REVISIONS file might not match since
886 blink snapshots point to tip of tree blink.
887 Note: The revisions_final variable might get modified to include
888 additional revisions."""
[email protected]37ed3172013-09-24 23:49:30889 blink_deps_rev = GetBlinkDEPSRevisionForChromiumRevision(rev)
890
891 while (GetBlinkRevisionForChromiumRevision(self, rev) > blink_deps_rev):
892 idx = revisions.index(rev)
893 if idx > 0:
894 rev = revisions[idx-1]
895 if rev not in revisions_final:
896 revisions_final.insert(0, rev)
897
898 revisions_final.sort()
899 return rev
[email protected]b2fe7f22011-10-25 22:58:31900
[email protected]4df583c2014-07-31 17:11:55901
[email protected]5980b752014-07-02 00:34:40902def GetChromiumRevision(context, url):
[email protected]801fb652012-07-20 20:13:50903 """Returns the chromium revision read from given URL."""
904 try:
905 # Location of the latest build revision number
[email protected]5980b752014-07-02 00:34:40906 latest_revision = urllib.urlopen(url).read()
907 if latest_revision.isdigit():
908 return int(latest_revision)
909 return context.GetSVNRevisionFromGitHash(latest_revision)
[email protected]4df583c2014-07-31 17:11:55910 except Exception:
911 print 'Could not determine latest revision. This could be bad...'
[email protected]801fb652012-07-20 20:13:50912 return 999999999
913
914
[email protected]67e0bc62009-09-03 22:06:09915def main():
[email protected]2c1d2732009-10-29 19:52:17916 usage = ('%prog [options] [-- chromium-options]\n'
[email protected]887c9182013-02-12 20:30:31917 'Perform binary search on the snapshot builds to find a minimal\n'
918 'range of revisions where a behavior change happened. The\n'
919 'behaviors are described as "good" and "bad".\n'
920 'It is NOT assumed that the behavior of the later revision is\n'
[email protected]09c58da2013-01-07 21:30:17921 'the bad one.\n'
[email protected]178aab72010-10-08 17:21:38922 '\n'
[email protected]887c9182013-02-12 20:30:31923 'Revision numbers should use\n'
924 ' Official versions (e.g. 1.0.1000.0) for official builds. (-o)\n'
925 ' SVN revisions (e.g. 123456) for chromium builds, from trunk.\n'
926 ' Use base_trunk_revision from http://omahaproxy.appspot.com/\n'
927 ' for earlier revs.\n'
928 ' Chrome\'s about: build number and omahaproxy branch_revision\n'
929 ' are incorrect, they are from branches.\n'
930 '\n'
[email protected]178aab72010-10-08 17:21:38931 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
[email protected]7ad66a72009-09-04 17:52:33932 parser = optparse.OptionParser(usage=usage)
[email protected]1a45d222009-09-19 01:58:57933 # Strangely, the default help output doesn't include the choice list.
[email protected]7aec9e82013-05-09 05:09:23934 choices = ['mac', 'win', 'linux', 'linux64', 'linux-arm']
[email protected]4082b182011-05-02 20:30:17935 # linux-chromiumos lacks a continuous archive http://crbug.com/78158
[email protected]7ad66a72009-09-04 17:52:33936 parser.add_option('-a', '--archive',
[email protected]4df583c2014-07-31 17:11:55937 choices=choices,
938 help='The buildbot archive to bisect [%s].' %
939 '|'.join(choices))
940 parser.add_option('-o',
941 action='store_true',
942 dest='official_builds',
943 help='Bisect across official Chrome builds (internal '
944 'only) instead of Chromium archives.')
945 parser.add_option('-b', '--bad',
946 type='str',
947 help='A bad revision to start bisection. '
948 'May be earlier or later than the good revision. '
949 'Default is HEAD.')
950 parser.add_option('-f', '--flash_path',
951 type='str',
952 help='Absolute path to a recent Adobe Pepper Flash '
953 'binary to be used in this bisection (e.g. '
954 'on Windows C:\...\pepflashplayer.dll and on Linux '
955 '/opt/google/chrome/PepperFlash/'
956 'libpepflashplayer.so).')
957 parser.add_option('-d', '--pdf_path',
958 type='str',
959 help='Absolute path to a recent PDF plugin '
960 'binary to be used in this bisection (e.g. '
961 'on Windows C:\...\pdf.dll and on Linux '
962 '/opt/google/chrome/libpdf.so). Option also enables '
963 'print preview.')
964 parser.add_option('-g', '--good',
965 type='str',
966 help='A good revision to start bisection. ' +
967 'May be earlier or later than the bad revision. ' +
968 'Default is 0.')
969 parser.add_option('-p', '--profile', '--user-data-dir',
970 type='str',
971 default='profile',
972 help='Profile to use; this will not reset every run. '
973 'Defaults to a clean profile.')
974 parser.add_option('-t', '--times',
975 type='int',
976 default=1,
977 help='Number of times to run each build before asking '
978 'if it\'s good or bad. Temporary profiles are reused.')
979 parser.add_option('-c', '--command',
980 type='str',
981 default='%p %a',
982 help='Command to execute. %p and %a refer to Chrome '
983 'executable and specified extra arguments '
984 'respectively. Use %s to specify all extra arguments '
985 'as one string. Defaults to "%p %a". Note that any '
986 'extra paths specified should be absolute.')
987 parser.add_option('-l', '--blink',
988 action='store_true',
989 help='Use Blink bisect instead of Chromium. ')
990 parser.add_option('', '--not-interactive',
991 action='store_true',
992 default=False,
993 help='Use command exit code to tell good/bad revision.')
[email protected]b3b20512013-08-26 18:51:04994 parser.add_option('--aura',
995 dest='aura',
996 action='store_true',
997 default=False,
998 help='Allow the script to bisect aura builds')
[email protected]011886692014-08-01 21:00:21999 parser.add_option('--asan',
1000 dest='asan',
1001 action='store_true',
1002 default=False,
1003 help='Allow the script to bisect ASAN builds')
[email protected]6a7a5d62014-07-09 04:45:501004 parser.add_option('--use-local-repo',
1005 dest='use_local_repo',
1006 action='store_true',
1007 default=False,
[email protected]4df583c2014-07-31 17:11:551008 help='Allow the script to convert git SHA1 to SVN '
1009 'revision using "git svn find-rev <SHA1>" '
1010 'command from a Chromium checkout.')
[email protected]b3b20512013-08-26 18:51:041011
[email protected]7ad66a72009-09-04 17:52:331012 (opts, args) = parser.parse_args()
1013
1014 if opts.archive is None:
[email protected]178aab72010-10-08 17:21:381015 print 'Error: missing required parameter: --archive'
1016 print
[email protected]7ad66a72009-09-04 17:52:331017 parser.print_help()
1018 return 1
1019
[email protected]b3b20512013-08-26 18:51:041020 if opts.aura:
1021 if opts.archive != 'win' or not opts.official_builds:
[email protected]4df583c2014-07-31 17:11:551022 print ('Error: Aura is supported only on Windows platform '
1023 'and official builds.')
[email protected]b3b20512013-08-26 18:51:041024 return 1
1025
[email protected]011886692014-08-01 21:00:211026 if opts.asan:
1027 supported_platforms = ['linux', 'mac', 'win']
1028 if opts.archive not in supported_platforms:
1029 print 'Error: ASAN bisecting only supported on these platforms: [%s].' % (
1030 '|'.join(supported_platforms))
1031 return 1
1032 if opts.official_builds:
1033 print 'Error: Do not yet support bisecting official ASAN builds.'
1034 return 1
1035
1036 if opts.asan:
1037 base_url = ASAN_BASE_URL
1038 elif opts.blink:
[email protected]4c6fec6b2013-09-17 17:44:081039 base_url = WEBKIT_BASE_URL
1040 else:
1041 base_url = CHROMIUM_BASE_URL
1042
[email protected]183706d92011-06-10 13:06:221043 # Create the context. Initialize 0 for the revisions as they are set below.
[email protected]2e0f2672014-08-13 20:32:581044 context = PathContext(base_url, opts.archive, opts.good, opts.bad,
[email protected]011886692014-08-01 21:00:211045 opts.official_builds, opts.aura, opts.asan,
[email protected]2e0f2672014-08-13 20:32:581046 opts.use_local_repo, opts.flash_path, opts.pdf_path)
[email protected]67e0bc62009-09-03 22:06:091047 # Pick a starting point, try to get HEAD for this.
[email protected]2e0f2672014-08-13 20:32:581048 if not opts.bad:
1049 context.bad_revision = '999.0.0.0'
1050 context.bad_revision = GetChromiumRevision(
1051 context, context.GetLastChangeURL())
[email protected]67e0bc62009-09-03 22:06:091052
1053 # Find out when we were good.
[email protected]2e0f2672014-08-13 20:32:581054 if not opts.good:
1055 context.good_revision = '0.0.0.0' if opts.official_builds else 0
[email protected]801fb652012-07-20 20:13:501056
[email protected]fc3702e2013-11-09 04:23:001057 if opts.flash_path:
[email protected]2e0f2672014-08-13 20:32:581058 msg = 'Could not find Flash binary at %s' % opts.flash_path
1059 assert os.path.exists(opts.flash_path), msg
[email protected]fc3702e2013-11-09 04:23:001060
[email protected]cdb77062014-07-21 18:07:151061 if opts.pdf_path:
[email protected]2e0f2672014-08-13 20:32:581062 msg = 'Could not find PDF binary at %s' % opts.pdf_path
1063 assert os.path.exists(opts.pdf_path), msg
[email protected]cdb77062014-07-21 18:07:151064
[email protected]801fb652012-07-20 20:13:501065 if opts.official_builds:
[email protected]2e0f2672014-08-13 20:32:581066 context.good_revision = LooseVersion(context.good_revision)
1067 context.bad_revision = LooseVersion(context.bad_revision)
[email protected]801fb652012-07-20 20:13:501068 else:
[email protected]2e0f2672014-08-13 20:32:581069 context.good_revision = int(context.good_revision)
1070 context.bad_revision = int(context.bad_revision)
[email protected]801fb652012-07-20 20:13:501071
[email protected]5e93cf162012-01-28 02:16:561072 if opts.times < 1:
1073 print('Number of times to run (%d) must be greater than or equal to 1.' %
1074 opts.times)
1075 parser.print_help()
1076 return 1
1077
[email protected]011886692014-08-01 21:00:211078 if opts.asan:
1079 evaluator = IsGoodASANBuild
1080 else:
1081 evaluator = AskIsGoodBuild
1082
[email protected]2e0f2672014-08-13 20:32:581083 # Save these revision numbers to compare when showing the changelog URL
1084 # after the bisect.
1085 good_rev = context.good_revision
1086 bad_rev = context.bad_revision
1087
1088 (min_chromium_rev, max_chromium_rev, context) = Bisect(
1089 context, opts.times, opts.command, args, opts.profile,
[email protected]011886692014-08-01 21:00:211090 not opts.not_interactive, evaluator)
[email protected]67e0bc62009-09-03 22:06:091091
[email protected]ff50d1c2013-04-17 18:49:361092 # Get corresponding blink revisions.
[email protected]b2fe7f22011-10-25 22:58:311093 try:
[email protected]4c6fec6b2013-09-17 17:44:081094 min_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1095 min_chromium_rev)
1096 max_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1097 max_chromium_rev)
[email protected]4df583c2014-07-31 17:11:551098 except Exception:
[email protected]b2fe7f22011-10-25 22:58:311099 # Silently ignore the failure.
[email protected]ff50d1c2013-04-17 18:49:361100 min_blink_rev, max_blink_rev = 0, 0
[email protected]b2fe7f22011-10-25 22:58:311101
[email protected]3bdaa4752013-09-30 20:13:361102 if opts.blink:
1103 # We're done. Let the user know the results in an official manner.
1104 if good_rev > bad_rev:
1105 print DONE_MESSAGE_GOOD_MAX % (str(min_blink_rev), str(max_blink_rev))
1106 else:
1107 print DONE_MESSAGE_GOOD_MIN % (str(min_blink_rev), str(max_blink_rev))
[email protected]eadd95d2012-11-02 22:42:091108
[email protected]ff50d1c2013-04-17 18:49:361109 print 'BLINK CHANGELOG URL:'
1110 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev)
[email protected]3bdaa4752013-09-30 20:13:361111
[email protected]d0149c5c2012-05-29 21:12:111112 else:
[email protected]3bdaa4752013-09-30 20:13:361113 # We're done. Let the user know the results in an official manner.
1114 if good_rev > bad_rev:
1115 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev),
1116 str(max_chromium_rev))
1117 else:
1118 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev),
1119 str(max_chromium_rev))
1120 if min_blink_rev != max_blink_rev:
[email protected]4df583c2014-07-31 17:11:551121 print ('NOTE: There is a Blink roll in the range, '
1122 'you might also want to do a Blink bisect.')
[email protected]3bdaa4752013-09-30 20:13:361123
1124 print 'CHANGELOG URL:'
1125 if opts.official_builds:
1126 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
1127 else:
1128 print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
[email protected]cb155a82011-11-29 17:25:341129
[email protected]4df583c2014-07-31 17:11:551130
[email protected]67e0bc62009-09-03 22:06:091131if __name__ == '__main__':
[email protected]7ad66a72009-09-04 17:52:331132 sys.exit(main())