blob: a87807e4d826a4662c4c5894d94b6a22ec848e07 [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# URL template for viewing changelogs between revisions.
pshenoy9ce271f2014-09-02 22:14:0524CHANGELOG_URL = ('https://chromium.googlesource.com/chromium/src/+log/%s..%s')
25
26# URL to convert SVN revision to git hash.
pshenoy13cb79e02014-09-05 01:42:5327CRREV_URL = ('https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/')
[email protected]f6a71a72009-10-08 19:55:3828
[email protected]b2fe7f22011-10-25 22:58:3129# DEPS file URL.
Di Mu08c59682016-07-11 23:05:0730DEPS_FILE = ('https://chromium.googlesource.com/chromium/src/+/%s/DEPS')
[email protected]b2fe7f22011-10-25 22:58:3131
[email protected]4df583c2014-07-31 17:11:5532# Blink changelogs URL.
33BLINK_CHANGELOG_URL = ('http://build.chromium.org'
34 '/f/chromium/perf/dashboard/ui/changelog_blink.html'
35 '?url=/trunk&range=%d%%3A%d')
36
37DONE_MESSAGE_GOOD_MIN = ('You are probably looking for a change made after %s ('
38 'known good), but no later than %s (first known bad).')
39DONE_MESSAGE_GOOD_MAX = ('You are probably looking for a change made after %s ('
40 'known bad), but no later than %s (first known good).')
[email protected]05ff3fd2012-04-17 23:24:0641
[email protected]3e7c85322014-06-27 20:27:3642CHROMIUM_GITHASH_TO_SVN_URL = (
43 'https://chromium.googlesource.com/chromium/src/+/%s?format=json')
[email protected]4df583c2014-07-31 17:11:5544
[email protected]3e7c85322014-06-27 20:27:3645BLINK_GITHASH_TO_SVN_URL = (
46 'https://chromium.googlesource.com/chromium/blink/+/%s?format=json')
[email protected]4df583c2014-07-31 17:11:5547
48GITHASH_TO_SVN_URL = {
49 'chromium': CHROMIUM_GITHASH_TO_SVN_URL,
50 'blink': BLINK_GITHASH_TO_SVN_URL,
51}
52
53# Search pattern to be matched in the JSON output from
[email protected]3e7c85322014-06-27 20:27:3654# CHROMIUM_GITHASH_TO_SVN_URL to get the chromium revision (svn revision).
pshenoyb23a1452014-09-05 22:52:0555CHROMIUM_SEARCH_PATTERN_OLD = (
[email protected]3e7c85322014-06-27 20:27:3656 r'.*git-svn-id: svn://svn.chromium.org/chrome/trunk/src@(\d+) ')
pshenoyb23a1452014-09-05 22:52:0557CHROMIUM_SEARCH_PATTERN = (
58 r'Cr-Commit-Position: refs/heads/master@{#(\d+)}')
[email protected]4df583c2014-07-31 17:11:5559
[email protected]3e7c85322014-06-27 20:27:3660# Search pattern to be matched in the json output from
61# BLINK_GITHASH_TO_SVN_URL to get the blink revision (svn revision).
62BLINK_SEARCH_PATTERN = (
63 r'.*git-svn-id: svn://svn.chromium.org/blink/trunk@(\d+) ')
[email protected]4df583c2014-07-31 17:11:5564
65SEARCH_PATTERN = {
66 'chromium': CHROMIUM_SEARCH_PATTERN,
67 'blink': BLINK_SEARCH_PATTERN,
68}
[email protected]3e7c85322014-06-27 20:27:3669
[email protected]480369782014-08-22 20:15:5870CREDENTIAL_ERROR_MESSAGE = ('You are attempting to access protected data with '
71 'no configured credentials')
72
[email protected]67e0bc62009-09-03 22:06:0973###############################################################################
74
[email protected]83048502014-08-21 16:48:4475import httplib
[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,
Jason Kersey97bb027a2016-05-11 20:10:4396 is_asan, use_local_cache, flash_path = None):
[email protected]183706d92011-06-10 13:06:2297 super(PathContext, self).__init__()
98 # Store off the input parameters.
[email protected]4c6fec6b2013-09-17 17:44:0899 self.base_url = base_url
[email protected]183706d92011-06-10 13:06:22100 self.platform = platform # What's passed in to the '-a/--archive' option.
101 self.good_revision = good_revision
102 self.bad_revision = bad_revision
[email protected]011886692014-08-01 21:00:21103 self.is_asan = is_asan
104 self.build_type = 'release'
[email protected]fc3702e2013-11-09 04:23:00105 self.flash_path = flash_path
[email protected]3e7c85322014-06-27 20:27:36106 # Dictionary which stores svn revision number as key and it's
107 # corresponding git hash as value. This data is populated in
108 # _FetchAndParse and used later in GetDownloadURL while downloading
109 # the build.
110 self.githash_svn_dict = {}
[email protected]183706d92011-06-10 13:06:22111 # The name of the ZIP file in a revision directory on the server.
112 self.archive_name = None
113
rob724c9062015-01-22 00:26:42114 # Whether to cache and use the list of known revisions in a local file to
115 # speed up the initialization of the script at the next run.
116 self.use_local_cache = use_local_cache
117
118 # Locate the local checkout to speed up the script by using locally stored
119 # metadata.
120 abs_file_path = os.path.abspath(os.path.realpath(__file__))
121 local_src_path = os.path.join(os.path.dirname(abs_file_path), '..')
122 if abs_file_path.endswith(os.path.join('tools', 'bisect-builds.py')) and\
123 os.path.exists(os.path.join(local_src_path, '.git')):
124 self.local_src_path = os.path.normpath(local_src_path)
125 else:
126 self.local_src_path = None
[email protected]6a7a5d62014-07-09 04:45:50127
[email protected]183706d92011-06-10 13:06:22128 # Set some internal members:
129 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'.
130 # _archive_extract_dir = Uncompressed directory in the archive_name file.
131 # _binary_name = The name of the executable to run.
dmazzoni76e907d2015-01-22 08:14:49132 if self.platform in ('linux', 'linux64', 'linux-arm', 'chromeos'):
[email protected]183706d92011-06-10 13:06:22133 self._binary_name = 'chrome'
[email protected]480369782014-08-22 20:15:58134 elif self.platform in ('mac', 'mac64'):
[email protected]183706d92011-06-10 13:06:22135 self.archive_name = 'chrome-mac.zip'
136 self._archive_extract_dir = 'chrome-mac'
[email protected]480369782014-08-22 20:15:58137 elif self.platform in ('win', 'win64'):
[email protected]183706d92011-06-10 13:06:22138 self.archive_name = 'chrome-win32.zip'
139 self._archive_extract_dir = 'chrome-win32'
140 self._binary_name = 'chrome.exe'
141 else:
[email protected]afe30662011-07-30 01:05:52142 raise Exception('Invalid platform: %s' % self.platform)
[email protected]183706d92011-06-10 13:06:22143
Jason Kersey97bb027a2016-05-11 20:10:43144 if self.platform in ('linux', 'linux64', 'linux-arm', 'chromeos'):
145 self.archive_name = 'chrome-linux.zip'
146 self._archive_extract_dir = 'chrome-linux'
[email protected]d0149c5c2012-05-29 21:12:11147 if self.platform == 'linux':
Jason Kersey97bb027a2016-05-11 20:10:43148 self._listing_platform_dir = 'Linux/'
[email protected]d0149c5c2012-05-29 21:12:11149 elif self.platform == 'linux64':
Jason Kersey97bb027a2016-05-11 20:10:43150 self._listing_platform_dir = 'Linux_x64/'
151 elif self.platform == 'linux-arm':
152 self._listing_platform_dir = 'Linux_ARM_Cross-Compile/'
153 elif self.platform == 'chromeos':
154 self._listing_platform_dir = 'Linux_ChromiumOS_Full/'
155 elif self.platform in ('mac', 'mac64'):
156 self._listing_platform_dir = 'Mac/'
157 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium'
158 elif self.platform == 'win':
159 self._listing_platform_dir = 'Win/'
jiawei.shao734efbc92016-09-23 02:11:45160 elif self.platform == 'win64':
161 self._listing_platform_dir = 'Win_x64/'
[email protected]d0149c5c2012-05-29 21:12:11162
[email protected]011886692014-08-01 21:00:21163 def GetASANPlatformDir(self):
164 """ASAN builds are in directories like "linux-release", or have filenames
165 like "asan-win32-release-277079.zip". This aligns to our platform names
166 except in the case of Windows where they use "win32" instead of "win"."""
167 if self.platform == 'win':
168 return 'win32'
169 else:
170 return self.platform
171
[email protected]183706d92011-06-10 13:06:22172 def GetListingURL(self, marker=None):
173 """Returns the URL for a directory listing, with an optional marker."""
174 marker_param = ''
175 if marker:
176 marker_param = '&marker=' + str(marker)
[email protected]011886692014-08-01 21:00:21177 if self.is_asan:
178 prefix = '%s-%s' % (self.GetASANPlatformDir(), self.build_type)
179 return self.base_url + '/?delimiter=&prefix=' + prefix + marker_param
180 else:
181 return (self.base_url + '/?delimiter=/&prefix=' +
182 self._listing_platform_dir + marker_param)
[email protected]183706d92011-06-10 13:06:22183
184 def GetDownloadURL(self, revision):
185 """Gets the download URL for a build archive of a specific revision."""
[email protected]011886692014-08-01 21:00:21186 if self.is_asan:
187 return '%s/%s-%s/%s-%d.zip' % (
188 ASAN_BASE_URL, self.GetASANPlatformDir(), self.build_type,
189 self.GetASANBaseName(), revision)
Jason Kersey97bb027a2016-05-11 20:10:43190 if str(revision) in self.githash_svn_dict:
191 revision = self.githash_svn_dict[str(revision)]
192 return '%s/%s%s/%s' % (self.base_url, self._listing_platform_dir,
193 revision, self.archive_name)
[email protected]183706d92011-06-10 13:06:22194
195 def GetLastChangeURL(self):
196 """Returns a URL to the LAST_CHANGE file."""
[email protected]4c6fec6b2013-09-17 17:44:08197 return self.base_url + '/' + self._listing_platform_dir + 'LAST_CHANGE'
[email protected]183706d92011-06-10 13:06:22198
[email protected]011886692014-08-01 21:00:21199 def GetASANBaseName(self):
200 """Returns the base name of the ASAN zip file."""
201 if 'linux' in self.platform:
202 return 'asan-symbolized-%s-%s' % (self.GetASANPlatformDir(),
203 self.build_type)
204 else:
205 return 'asan-%s-%s' % (self.GetASANPlatformDir(), self.build_type)
206
207 def GetLaunchPath(self, revision):
[email protected]183706d92011-06-10 13:06:22208 """Returns a relative path (presumably from the archive extraction location)
209 that is used to run the executable."""
[email protected]011886692014-08-01 21:00:21210 if self.is_asan:
211 extract_dir = '%s-%d' % (self.GetASANBaseName(), revision)
212 else:
213 extract_dir = self._archive_extract_dir
214 return os.path.join(extract_dir, self._binary_name)
[email protected]183706d92011-06-10 13:06:22215
rob724c9062015-01-22 00:26:42216 def ParseDirectoryIndex(self, last_known_rev):
[email protected]afe30662011-07-30 01:05:52217 """Parses the Google Storage directory listing into a list of revision
[email protected]eadd95d2012-11-02 22:42:09218 numbers."""
[email protected]afe30662011-07-30 01:05:52219
rob724c9062015-01-22 00:26:42220 def _GetMarkerForRev(revision):
221 if self.is_asan:
222 return '%s-%s/%s-%d.zip' % (
223 self.GetASANPlatformDir(), self.build_type,
224 self.GetASANBaseName(), revision)
225 return '%s%d' % (self._listing_platform_dir, revision)
226
[email protected]afe30662011-07-30 01:05:52227 def _FetchAndParse(url):
228 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If
229 next-marker is not None, then the listing is a partial listing and another
230 fetch should be performed with next-marker being the marker= GET
231 parameter."""
232 handle = urllib.urlopen(url)
233 document = ElementTree.parse(handle)
234
235 # All nodes in the tree are namespaced. Get the root's tag name to extract
236 # the namespace. Etree does namespaces as |{namespace}tag|.
237 root_tag = document.getroot().tag
238 end_ns_pos = root_tag.find('}')
239 if end_ns_pos == -1:
[email protected]4df583c2014-07-31 17:11:55240 raise Exception('Could not locate end namespace for directory index')
[email protected]afe30662011-07-30 01:05:52241 namespace = root_tag[:end_ns_pos + 1]
242
243 # Find the prefix (_listing_platform_dir) and whether or not the list is
244 # truncated.
245 prefix_len = len(document.find(namespace + 'Prefix').text)
246 next_marker = None
247 is_truncated = document.find(namespace + 'IsTruncated')
248 if is_truncated is not None and is_truncated.text.lower() == 'true':
249 next_marker = document.find(namespace + 'NextMarker').text
[email protected]afe30662011-07-30 01:05:52250 # Get a list of all the revisions.
[email protected]afe30662011-07-30 01:05:52251 revisions = []
[email protected]3e7c85322014-06-27 20:27:36252 githash_svn_dict = {}
[email protected]011886692014-08-01 21:00:21253 if self.is_asan:
254 asan_regex = re.compile(r'.*%s-(\d+)\.zip$' % (self.GetASANBaseName()))
255 # Non ASAN builds are in a <revision> directory. The ASAN builds are
256 # flat
257 all_prefixes = document.findall(namespace + 'Contents/' +
258 namespace + 'Key')
259 for prefix in all_prefixes:
260 m = asan_regex.match(prefix.text)
261 if m:
262 try:
263 revisions.append(int(m.group(1)))
264 except ValueError:
265 pass
266 else:
267 all_prefixes = document.findall(namespace + 'CommonPrefixes/' +
268 namespace + 'Prefix')
269 # The <Prefix> nodes have content of the form of
270 # |_listing_platform_dir/revision/|. Strip off the platform dir and the
271 # trailing slash to just have a number.
272 for prefix in all_prefixes:
273 revnum = prefix.text[prefix_len:-1]
274 try:
dimua1dfa0ce2016-03-31 01:08:45275 revnum = int(revnum)
276 revisions.append(revnum)
277 # Notes:
278 # Ignore hash in chromium-browser-snapshots as they are invalid
279 # Resulting in 404 error in fetching pages:
280 # https://chromium.googlesource.com/chromium/src/+/[rev_hash]
[email protected]011886692014-08-01 21:00:21281 except ValueError:
282 pass
[email protected]3e7c85322014-06-27 20:27:36283 return (revisions, next_marker, githash_svn_dict)
[email protected]9639b002013-08-30 14:45:52284
[email protected]afe30662011-07-30 01:05:52285 # Fetch the first list of revisions.
rob724c9062015-01-22 00:26:42286 if last_known_rev:
287 revisions = []
288 # Optimization: Start paging at the last known revision (local cache).
289 next_marker = _GetMarkerForRev(last_known_rev)
290 # Optimization: Stop paging at the last known revision (remote).
291 last_change_rev = GetChromiumRevision(self, self.GetLastChangeURL())
292 if last_known_rev == last_change_rev:
293 return []
294 else:
295 (revisions, next_marker, new_dict) = _FetchAndParse(self.GetListingURL())
296 self.githash_svn_dict.update(new_dict)
297 last_change_rev = None
298
[email protected]afe30662011-07-30 01:05:52299 # If the result list was truncated, refetch with the next marker. Do this
300 # until an entire directory listing is done.
301 while next_marker:
rob724c9062015-01-22 00:26:42302 sys.stdout.write('\rFetching revisions at marker %s' % next_marker)
303 sys.stdout.flush()
304
[email protected]afe30662011-07-30 01:05:52305 next_url = self.GetListingURL(next_marker)
[email protected]3e7c85322014-06-27 20:27:36306 (new_revisions, next_marker, new_dict) = _FetchAndParse(next_url)
[email protected]afe30662011-07-30 01:05:52307 revisions.extend(new_revisions)
[email protected]3e7c85322014-06-27 20:27:36308 self.githash_svn_dict.update(new_dict)
rob724c9062015-01-22 00:26:42309 if last_change_rev and last_change_rev in new_revisions:
310 break
311 sys.stdout.write('\r')
312 sys.stdout.flush()
[email protected]afe30662011-07-30 01:05:52313 return revisions
314
[email protected]6a7a5d62014-07-09 04:45:50315 def _GetSVNRevisionFromGitHashWithoutGitCheckout(self, git_sha1, depot):
[email protected]3e7c85322014-06-27 20:27:36316 json_url = GITHASH_TO_SVN_URL[depot] % git_sha1
[email protected]2e0f2672014-08-13 20:32:58317 response = urllib.urlopen(json_url)
318 if response.getcode() == 200:
319 try:
320 data = json.loads(response.read()[4:])
321 except ValueError:
322 print 'ValueError for JSON URL: %s' % json_url
323 raise ValueError
324 else:
325 raise ValueError
[email protected]3e7c85322014-06-27 20:27:36326 if 'message' in data:
327 message = data['message'].split('\n')
328 message = [line for line in message if line.strip()]
329 search_pattern = re.compile(SEARCH_PATTERN[depot])
330 result = search_pattern.search(message[len(message)-1])
331 if result:
332 return result.group(1)
pshenoyb23a1452014-09-05 22:52:05333 else:
334 if depot == 'chromium':
335 result = re.search(CHROMIUM_SEARCH_PATTERN_OLD,
336 message[len(message)-1])
337 if result:
338 return result.group(1)
[email protected]3e7c85322014-06-27 20:27:36339 print 'Failed to get svn revision number for %s' % git_sha1
[email protected]1f99f4d2014-07-23 16:44:14340 raise ValueError
[email protected]3e7c85322014-06-27 20:27:36341
[email protected]6a7a5d62014-07-09 04:45:50342 def _GetSVNRevisionFromGitHashFromGitCheckout(self, git_sha1, depot):
343 def _RunGit(command, path):
344 command = ['git'] + command
[email protected]6a7a5d62014-07-09 04:45:50345 shell = sys.platform.startswith('win')
346 proc = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE,
rob724c9062015-01-22 00:26:42347 stderr=subprocess.PIPE, cwd=path)
[email protected]6a7a5d62014-07-09 04:45:50348 (output, _) = proc.communicate()
[email protected]6a7a5d62014-07-09 04:45:50349 return (output, proc.returncode)
350
rob724c9062015-01-22 00:26:42351 path = self.local_src_path
[email protected]6a7a5d62014-07-09 04:45:50352 if depot == 'blink':
rob724c9062015-01-22 00:26:42353 path = os.path.join(self.local_src_path, 'third_party', 'WebKit')
354 revision = None
355 try:
[email protected]6a7a5d62014-07-09 04:45:50356 command = ['svn', 'find-rev', git_sha1]
357 (git_output, return_code) = _RunGit(command, path)
358 if not return_code:
rob724c9062015-01-22 00:26:42359 revision = git_output.strip('\n')
360 except ValueError:
361 pass
362 if not revision:
363 command = ['log', '-n1', '--format=%s', git_sha1]
364 (git_output, return_code) = _RunGit(command, path)
365 if not return_code:
366 revision = re.match('SVN changes up to revision ([0-9]+)', git_output)
367 revision = revision.group(1) if revision else None
368 if revision:
369 return revision
370 raise ValueError
[email protected]6a7a5d62014-07-09 04:45:50371
372 def GetSVNRevisionFromGitHash(self, git_sha1, depot='chromium'):
rob724c9062015-01-22 00:26:42373 if not self.local_src_path:
[email protected]6a7a5d62014-07-09 04:45:50374 return self._GetSVNRevisionFromGitHashWithoutGitCheckout(git_sha1, depot)
375 else:
376 return self._GetSVNRevisionFromGitHashFromGitCheckout(git_sha1, depot)
377
[email protected]afe30662011-07-30 01:05:52378 def GetRevList(self):
379 """Gets the list of revision numbers between self.good_revision and
380 self.bad_revision."""
rob724c9062015-01-22 00:26:42381
382 cache = {}
383 # The cache is stored in the same directory as bisect-builds.py
384 cache_filename = os.path.join(
385 os.path.abspath(os.path.dirname(__file__)),
386 '.bisect-builds-cache.json')
387 cache_dict_key = self.GetListingURL()
388
389 def _LoadBucketFromCache():
390 if self.use_local_cache:
391 try:
392 with open(cache_filename) as cache_file:
rob1c836052015-05-18 16:34:02393 for (key, value) in json.load(cache_file).items():
394 cache[key] = value
rob724c9062015-01-22 00:26:42395 revisions = cache.get(cache_dict_key, [])
396 githash_svn_dict = cache.get('githash_svn_dict', {})
397 if revisions:
398 print 'Loaded revisions %d-%d from %s' % (revisions[0],
399 revisions[-1], cache_filename)
400 return (revisions, githash_svn_dict)
401 except (EnvironmentError, ValueError):
402 pass
403 return ([], {})
404
405 def _SaveBucketToCache():
406 """Save the list of revisions and the git-svn mappings to a file.
407 The list of revisions is assumed to be sorted."""
408 if self.use_local_cache:
409 cache[cache_dict_key] = revlist_all
410 cache['githash_svn_dict'] = self.githash_svn_dict
411 try:
412 with open(cache_filename, 'w') as cache_file:
413 json.dump(cache, cache_file)
414 print 'Saved revisions %d-%d to %s' % (
415 revlist_all[0], revlist_all[-1], cache_filename)
416 except EnvironmentError:
417 pass
418
[email protected]afe30662011-07-30 01:05:52419 # Download the revlist and filter for just the range between good and bad.
[email protected]eadd95d2012-11-02 22:42:09420 minrev = min(self.good_revision, self.bad_revision)
421 maxrev = max(self.good_revision, self.bad_revision)
rob724c9062015-01-22 00:26:42422
423 (revlist_all, self.githash_svn_dict) = _LoadBucketFromCache()
424 last_known_rev = revlist_all[-1] if revlist_all else 0
425 if last_known_rev < maxrev:
426 revlist_all.extend(map(int, self.ParseDirectoryIndex(last_known_rev)))
427 revlist_all = list(set(revlist_all))
428 revlist_all.sort()
429 _SaveBucketToCache()
[email protected]37ed3172013-09-24 23:49:30430
431 revlist = [x for x in revlist_all if x >= int(minrev) and x <= int(maxrev)]
[email protected]37ed3172013-09-24 23:49:30432
433 # Set good and bad revisions to be legit revisions.
434 if revlist:
435 if self.good_revision < self.bad_revision:
436 self.good_revision = revlist[0]
437 self.bad_revision = revlist[-1]
438 else:
439 self.bad_revision = revlist[0]
440 self.good_revision = revlist[-1]
441
442 # Fix chromium rev so that the deps blink revision matches REVISIONS file.
443 if self.base_url == WEBKIT_BASE_URL:
444 revlist_all.sort()
445 self.good_revision = FixChromiumRevForBlink(revlist,
446 revlist_all,
447 self,
448 self.good_revision)
449 self.bad_revision = FixChromiumRevForBlink(revlist,
450 revlist_all,
451 self,
452 self.bad_revision)
[email protected]afe30662011-07-30 01:05:52453 return revlist
454
prasadv2375e6d2017-03-20 19:23:23455
456def IsMac():
457 return sys.platform.startswith('darwin')
458
459
[email protected]fc3702e2013-11-09 04:23:00460def UnzipFilenameToDir(filename, directory):
461 """Unzip |filename| to |directory|."""
[email protected]afe30662011-07-30 01:05:52462 cwd = os.getcwd()
463 if not os.path.isabs(filename):
464 filename = os.path.join(cwd, filename)
[email protected]bd8dcb92010-03-31 01:05:24465 # Make base.
[email protected]fc3702e2013-11-09 04:23:00466 if not os.path.isdir(directory):
467 os.mkdir(directory)
468 os.chdir(directory)
prasadv2375e6d2017-03-20 19:23:23469
470 # The Python ZipFile does not support symbolic links, which makes it
471 # unsuitable for Mac builds. so use ditto instead.
472 if IsMac():
473 unzip_cmd = ['ditto', '-x', '-k', filename, '.']
474 proc = subprocess.Popen(unzip_cmd, bufsize=0, stdout=subprocess.PIPE,
475 stderr=subprocess.PIPE)
476 proc.communicate()
477 os.chdir(cwd)
478 return
479
480 zf = zipfile.ZipFile(filename)
[email protected]e29c08c2012-09-17 20:50:50481 # Extract files.
482 for info in zf.infolist():
483 name = info.filename
484 if name.endswith('/'): # dir
485 if not os.path.isdir(name):
486 os.makedirs(name)
487 else: # file
[email protected]fc3702e2013-11-09 04:23:00488 directory = os.path.dirname(name)
John Budorick06e5df12015-02-27 17:44:27489 if not os.path.isdir(directory):
[email protected]fc3702e2013-11-09 04:23:00490 os.makedirs(directory)
[email protected]e29c08c2012-09-17 20:50:50491 out = open(name, 'wb')
492 out.write(zf.read(name))
493 out.close()
494 # Set permissions. Permission info in external_attr is shifted 16 bits.
495 os.chmod(name, info.external_attr >> 16L)
496 os.chdir(cwd)
[email protected]bd8dcb92010-03-31 01:05:24497
[email protected]67e0bc62009-09-03 22:06:09498
[email protected]468a9772011-08-09 18:42:00499def FetchRevision(context, rev, filename, quit_event=None, progress_event=None):
[email protected]afe30662011-07-30 01:05:52500 """Downloads and unzips revision |rev|.
501 @param context A PathContext instance.
502 @param rev The Chromium revision number/tag to download.
503 @param filename The destination for the downloaded file.
504 @param quit_event A threading.Event which will be set by the master thread to
505 indicate that the download should be aborted.
[email protected]468a9772011-08-09 18:42:00506 @param progress_event A threading.Event which will be set by the master thread
507 to indicate that the progress of the download should be
508 displayed.
[email protected]afe30662011-07-30 01:05:52509 """
510 def ReportHook(blocknum, blocksize, totalsize):
[email protected]946be752011-10-25 23:34:21511 if quit_event and quit_event.isSet():
[email protected]4df583c2014-07-31 17:11:55512 raise RuntimeError('Aborting download of revision %s' % str(rev))
[email protected]946be752011-10-25 23:34:21513 if progress_event and progress_event.isSet():
[email protected]468a9772011-08-09 18:42:00514 size = blocknum * blocksize
515 if totalsize == -1: # Total size not known.
[email protected]4df583c2014-07-31 17:11:55516 progress = 'Received %d bytes' % size
[email protected]468a9772011-08-09 18:42:00517 else:
518 size = min(totalsize, size)
[email protected]4df583c2014-07-31 17:11:55519 progress = 'Received %d of %d bytes, %.2f%%' % (
[email protected]468a9772011-08-09 18:42:00520 size, totalsize, 100.0 * size / totalsize)
521 # Send a \r to let all progress messages use just one line of output.
[email protected]4df583c2014-07-31 17:11:55522 sys.stdout.write('\r' + progress)
[email protected]468a9772011-08-09 18:42:00523 sys.stdout.flush()
[email protected]afe30662011-07-30 01:05:52524 download_url = context.GetDownloadURL(rev)
525 try:
John Budorick06e5df12015-02-27 17:44:27526 urllib.urlretrieve(download_url, filename, ReportHook)
[email protected]946be752011-10-25 23:34:21527 if progress_event and progress_event.isSet():
[email protected]ecaba01e62011-10-26 05:33:28528 print
mikecasee2b6ce82015-02-06 18:22:39529
[email protected]4df583c2014-07-31 17:11:55530 except RuntimeError:
[email protected]afe30662011-07-30 01:05:52531 pass
[email protected]7ad66a72009-09-04 17:52:33532
[email protected]7ad66a72009-09-04 17:52:33533
[email protected]4df583c2014-07-31 17:11:55534def RunRevision(context, revision, zip_file, profile, num_runs, command, args):
[email protected]afe30662011-07-30 01:05:52535 """Given a zipped revision, unzip it and run the test."""
[email protected]4df583c2014-07-31 17:11:55536 print 'Trying revision %s...' % str(revision)
[email protected]3ff00b72011-07-20 21:34:47537
[email protected]afe30662011-07-30 01:05:52538 # Create a temp directory and unzip the revision into it.
[email protected]7ad66a72009-09-04 17:52:33539 cwd = os.getcwd()
540 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
[email protected]4df583c2014-07-31 17:11:55541 UnzipFilenameToDir(zip_file, tempdir)
dmazzoni76e907d2015-01-22 08:14:49542
543 # Hack: Chrome OS archives are missing icudtl.dat; try to copy it from
544 # the local directory.
545 if context.platform == 'chromeos':
oshima32b31c52016-07-01 18:30:54546 icudtl_path = 'third_party/icu/common/icudtl.dat'
dmazzoni76e907d2015-01-22 08:14:49547 if not os.access(icudtl_path, os.F_OK):
548 print 'Couldn\'t find: ' + icudtl_path
oshima32b31c52016-07-01 18:30:54549 print ('The path might have changed. Please look for the data under '
550 'third_party/icu and update bisect-build.py')
dmazzoni76e907d2015-01-22 08:14:49551 sys.exit()
552 os.system('cp %s %s/chrome-linux/' % (icudtl_path, tempdir))
553
[email protected]7ad66a72009-09-04 17:52:33554 os.chdir(tempdir)
[email protected]67e0bc62009-09-03 22:06:09555
[email protected]5e93cf162012-01-28 02:16:56556 # Run the build as many times as specified.
[email protected]4646a752013-07-19 22:14:34557 testargs = ['--user-data-dir=%s' % profile] + args
[email protected]d0149c5c2012-05-29 21:12:11558 # The sandbox must be run as root on Official Chrome, so bypass it.
Jason Kersey97bb027a2016-05-11 20:10:43559 if (context.flash_path and context.platform.startswith('linux')):
[email protected]d0149c5c2012-05-29 21:12:11560 testargs.append('--no-sandbox')
[email protected]fc3702e2013-11-09 04:23:00561 if context.flash_path:
562 testargs.append('--ppapi-flash-path=%s' % context.flash_path)
563 # We have to pass a large enough Flash version, which currently needs not
564 # be correct. Instead of requiring the user of the script to figure out and
565 # pass the correct version we just spoof it.
566 testargs.append('--ppapi-flash-version=99.9.999.999')
[email protected]d0149c5c2012-05-29 21:12:11567
[email protected]4646a752013-07-19 22:14:34568 runcommand = []
[email protected]61ea90a2013-09-26 10:17:34569 for token in shlex.split(command):
[email protected]4df583c2014-07-31 17:11:55570 if token == '%a':
[email protected]4646a752013-07-19 22:14:34571 runcommand.extend(testargs)
572 else:
[email protected]4df583c2014-07-31 17:11:55573 runcommand.append(
[email protected]011886692014-08-01 21:00:21574 token.replace('%p', os.path.abspath(context.GetLaunchPath(revision))).
575 replace('%s', ' '.join(testargs)))
[email protected]4646a752013-07-19 22:14:34576
[email protected]d59c8712014-02-11 21:04:57577 results = []
[email protected]4df583c2014-07-31 17:11:55578 for _ in range(num_runs):
[email protected]4646a752013-07-19 22:14:34579 subproc = subprocess.Popen(runcommand,
[email protected]5e93cf162012-01-28 02:16:56580 bufsize=-1,
581 stdout=subprocess.PIPE,
582 stderr=subprocess.PIPE)
583 (stdout, stderr) = subproc.communicate()
[email protected]d59c8712014-02-11 21:04:57584 results.append((subproc.returncode, stdout, stderr))
[email protected]7ad66a72009-09-04 17:52:33585 os.chdir(cwd)
[email protected]7ad66a72009-09-04 17:52:33586 try:
587 shutil.rmtree(tempdir, True)
[email protected]4df583c2014-07-31 17:11:55588 except Exception:
[email protected]7ad66a72009-09-04 17:52:33589 pass
[email protected]67e0bc62009-09-03 22:06:09590
[email protected]d59c8712014-02-11 21:04:57591 for (returncode, stdout, stderr) in results:
592 if returncode:
593 return (returncode, stdout, stderr)
594 return results[0]
[email protected]79f14742010-03-10 01:01:57595
[email protected]cb155a82011-11-29 17:25:34596
Jason Kersey97bb027a2016-05-11 20:10:43597# The arguments status, stdout and stderr are unused.
[email protected]4df583c2014-07-31 17:11:55598# They are present here because this function is passed to Bisect which then
599# calls it with 5 arguments.
600# pylint: disable=W0613
Jason Kersey97bb027a2016-05-11 20:10:43601def AskIsGoodBuild(rev, exit_status, stdout, stderr):
[email protected]4df583c2014-07-31 17:11:55602 """Asks the user whether build |rev| is good or bad."""
[email protected]79f14742010-03-10 01:01:57603 # Loop until we get a response that we can parse.
[email protected]67e0bc62009-09-03 22:06:09604 while True:
[email protected]4df583c2014-07-31 17:11:55605 response = raw_input('Revision %s is '
wangxianzhud8c4c562015-12-15 23:39:51606 '[(g)ood/(b)ad/(r)etry/(u)nknown/(s)tdout/(q)uit]: ' %
[email protected]53bb6342012-06-01 04:11:00607 str(rev))
wangxianzhud8c4c562015-12-15 23:39:51608 if response in ('g', 'b', 'r', 'u'):
[email protected]53bb6342012-06-01 04:11:00609 return response
wangxianzhud8c4c562015-12-15 23:39:51610 if response == 'q':
[email protected]afe30662011-07-30 01:05:52611 raise SystemExit()
wangxianzhud8c4c562015-12-15 23:39:51612 if response == 's':
613 print stdout
614 print stderr
[email protected]67e0bc62009-09-03 22:06:09615
[email protected]cb155a82011-11-29 17:25:34616
Jason Kersey97bb027a2016-05-11 20:10:43617def IsGoodASANBuild(rev, exit_status, stdout, stderr):
[email protected]011886692014-08-01 21:00:21618 """Determine if an ASAN build |rev| is good or bad
619
620 Will examine stderr looking for the error message emitted by ASAN. If not
621 found then will fallback to asking the user."""
622 if stderr:
623 bad_count = 0
624 for line in stderr.splitlines():
625 print line
626 if line.find('ERROR: AddressSanitizer:') != -1:
627 bad_count += 1
628 if bad_count > 0:
629 print 'Revision %d determined to be bad.' % rev
630 return 'b'
Jason Kersey97bb027a2016-05-11 20:10:43631 return AskIsGoodBuild(rev, exit_status, stdout, stderr)
skobes21b5cdfb2016-03-21 23:13:02632
633
Jason Kersey97bb027a2016-05-11 20:10:43634def DidCommandSucceed(rev, exit_status, stdout, stderr):
skobes21b5cdfb2016-03-21 23:13:02635 if exit_status:
636 print 'Bad revision: %s' % rev
637 return 'b'
638 else:
639 print 'Good revision: %s' % rev
640 return 'g'
641
[email protected]011886692014-08-01 21:00:21642
[email protected]53bb6342012-06-01 04:11:00643class DownloadJob(object):
644 """DownloadJob represents a task to download a given Chromium revision."""
[email protected]4df583c2014-07-31 17:11:55645
646 def __init__(self, context, name, rev, zip_file):
[email protected]53bb6342012-06-01 04:11:00647 super(DownloadJob, self).__init__()
648 # Store off the input parameters.
649 self.context = context
650 self.name = name
651 self.rev = rev
[email protected]4df583c2014-07-31 17:11:55652 self.zip_file = zip_file
[email protected]53bb6342012-06-01 04:11:00653 self.quit_event = threading.Event()
654 self.progress_event = threading.Event()
[email protected]4df583c2014-07-31 17:11:55655 self.thread = None
[email protected]53bb6342012-06-01 04:11:00656
657 def Start(self):
658 """Starts the download."""
659 fetchargs = (self.context,
660 self.rev,
[email protected]4df583c2014-07-31 17:11:55661 self.zip_file,
[email protected]53bb6342012-06-01 04:11:00662 self.quit_event,
663 self.progress_event)
664 self.thread = threading.Thread(target=FetchRevision,
665 name=self.name,
666 args=fetchargs)
667 self.thread.start()
668
669 def Stop(self):
670 """Stops the download which must have been started previously."""
[email protected]4df583c2014-07-31 17:11:55671 assert self.thread, 'DownloadJob must be started before Stop is called.'
[email protected]53bb6342012-06-01 04:11:00672 self.quit_event.set()
673 self.thread.join()
[email protected]4df583c2014-07-31 17:11:55674 os.unlink(self.zip_file)
[email protected]53bb6342012-06-01 04:11:00675
676 def WaitFor(self):
677 """Prints a message and waits for the download to complete. The download
678 must have been started previously."""
[email protected]4df583c2014-07-31 17:11:55679 assert self.thread, 'DownloadJob must be started before WaitFor is called.'
680 print 'Downloading revision %s...' % str(self.rev)
[email protected]53bb6342012-06-01 04:11:00681 self.progress_event.set() # Display progress of download.
rob8a4543f2016-01-20 00:43:59682 try:
683 while self.thread.isAlive():
684 # The parameter to join is needed to keep the main thread responsive to
685 # signals. Without it, the program will not respond to interruptions.
686 self.thread.join(1)
687 except (KeyboardInterrupt, SystemExit):
688 self.Stop()
689 raise
[email protected]53bb6342012-06-01 04:11:00690
691
skobes21b5cdfb2016-03-21 23:13:02692def VerifyEndpoint(fetch, context, rev, profile, num_runs, command, try_args,
693 evaluate, expected_answer):
694 fetch.WaitFor()
695 try:
696 (exit_status, stdout, stderr) = RunRevision(
697 context, rev, fetch.zip_file, profile, num_runs, command, try_args)
698 except Exception, e:
699 print >> sys.stderr, e
Jason Kersey97bb027a2016-05-11 20:10:43700 if (evaluate(rev, exit_status, stdout, stderr) != expected_answer):
skobes21b5cdfb2016-03-21 23:13:02701 print 'Unexpected result at a range boundary! Your range is not correct.'
702 raise SystemExit
703
704
[email protected]2e0f2672014-08-13 20:32:58705def Bisect(context,
[email protected]5e93cf162012-01-28 02:16:56706 num_runs=1,
[email protected]4df583c2014-07-31 17:11:55707 command='%p %a',
[email protected]60ac66e32011-07-18 16:08:25708 try_args=(),
[email protected]afe30662011-07-30 01:05:52709 profile=None,
skobes21b5cdfb2016-03-21 23:13:02710 evaluate=AskIsGoodBuild,
711 verify_range=False):
[email protected]afe30662011-07-30 01:05:52712 """Given known good and known bad revisions, run a binary search on all
713 archived revisions to determine the last known good revision.
[email protected]60ac66e32011-07-18 16:08:25714
[email protected]2e0f2672014-08-13 20:32:58715 @param context PathContext object initialized with user provided parameters.
[email protected]5e93cf162012-01-28 02:16:56716 @param num_runs Number of times to run each build for asking good/bad.
[email protected]afe30662011-07-30 01:05:52717 @param try_args A tuple of arguments to pass to the test application.
718 @param profile The name of the user profile to run with.
[email protected]53bb6342012-06-01 04:11:00719 @param evaluate A function which returns 'g' if the argument build is good,
720 'b' if it's bad or 'u' if unknown.
skobes21b5cdfb2016-03-21 23:13:02721 @param verify_range If true, tests the first and last revisions in the range
722 before proceeding with the bisect.
[email protected]afe30662011-07-30 01:05:52723
724 Threading is used to fetch Chromium revisions in the background, speeding up
725 the user's experience. For example, suppose the bounds of the search are
726 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on
727 whether revision 50 is good or bad, the next revision to check will be either
728 25 or 75. So, while revision 50 is being checked, the script will download
729 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is
730 known:
731
732 - If rev 50 is good, the download of rev 25 is cancelled, and the next test
733 is run on rev 75.
734
735 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
736 is run on rev 25.
[email protected]60ac66e32011-07-18 16:08:25737 """
738
[email protected]afe30662011-07-30 01:05:52739 if not profile:
740 profile = 'profile'
741
[email protected]2e0f2672014-08-13 20:32:58742 good_rev = context.good_revision
743 bad_rev = context.bad_revision
[email protected]afe30662011-07-30 01:05:52744 cwd = os.getcwd()
745
[email protected]28a3c122014-08-09 11:04:51746 print 'Downloading list of known revisions...',
Jason Kersey97bb027a2016-05-11 20:10:43747 if not context.use_local_cache:
rob724c9062015-01-22 00:26:42748 print '(use --use-local-cache to cache and re-use the list of revisions)'
[email protected]28a3c122014-08-09 11:04:51749 else:
750 print
[email protected]d0149c5c2012-05-29 21:12:11751 _GetDownloadPath = lambda rev: os.path.join(cwd,
752 '%s-%s' % (str(rev), context.archive_name))
Jason Kersey97bb027a2016-05-11 20:10:43753 revlist = context.GetRevList()
[email protected]afe30662011-07-30 01:05:52754
755 # Get a list of revisions to bisect across.
756 if len(revlist) < 2: # Don't have enough builds to bisect.
757 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist
758 raise RuntimeError(msg)
759
760 # Figure out our bookends and first pivot point; fetch the pivot revision.
[email protected]eadd95d2012-11-02 22:42:09761 minrev = 0
762 maxrev = len(revlist) - 1
763 pivot = maxrev / 2
[email protected]afe30662011-07-30 01:05:52764 rev = revlist[pivot]
skobes21b5cdfb2016-03-21 23:13:02765 fetch = DownloadJob(context, 'initial_fetch', rev, _GetDownloadPath(rev))
[email protected]eadd95d2012-11-02 22:42:09766 fetch.Start()
skobes21b5cdfb2016-03-21 23:13:02767
768 if verify_range:
769 minrev_fetch = DownloadJob(
770 context, 'minrev_fetch', revlist[minrev],
771 _GetDownloadPath(revlist[minrev]))
772 maxrev_fetch = DownloadJob(
773 context, 'maxrev_fetch', revlist[maxrev],
774 _GetDownloadPath(revlist[maxrev]))
775 minrev_fetch.Start()
776 maxrev_fetch.Start()
777 try:
778 VerifyEndpoint(minrev_fetch, context, revlist[minrev], profile, num_runs,
779 command, try_args, evaluate, 'b' if bad_rev < good_rev else 'g')
780 VerifyEndpoint(maxrev_fetch, context, revlist[maxrev], profile, num_runs,
781 command, try_args, evaluate, 'g' if bad_rev < good_rev else 'b')
782 except (KeyboardInterrupt, SystemExit):
783 print 'Cleaning up...'
784 fetch.Stop()
785 sys.exit(0)
786 finally:
787 minrev_fetch.Stop()
788 maxrev_fetch.Stop()
789
[email protected]eadd95d2012-11-02 22:42:09790 fetch.WaitFor()
[email protected]60ac66e32011-07-18 16:08:25791
792 # Binary search time!
[email protected]4df583c2014-07-31 17:11:55793 while fetch and fetch.zip_file and maxrev - minrev > 1:
[email protected]eadd95d2012-11-02 22:42:09794 if bad_rev < good_rev:
[email protected]4df583c2014-07-31 17:11:55795 min_str, max_str = 'bad', 'good'
[email protected]eadd95d2012-11-02 22:42:09796 else:
[email protected]4df583c2014-07-31 17:11:55797 min_str, max_str = 'good', 'bad'
798 print 'Bisecting range [%s (%s), %s (%s)].' % (revlist[minrev], min_str,
[email protected]eadd95d2012-11-02 22:42:09799 revlist[maxrev], max_str)
800
[email protected]afe30662011-07-30 01:05:52801 # Pre-fetch next two possible pivots
802 # - down_pivot is the next revision to check if the current revision turns
803 # out to be bad.
804 # - up_pivot is the next revision to check if the current revision turns
805 # out to be good.
[email protected]eadd95d2012-11-02 22:42:09806 down_pivot = int((pivot - minrev) / 2) + minrev
[email protected]53bb6342012-06-01 04:11:00807 down_fetch = None
[email protected]eadd95d2012-11-02 22:42:09808 if down_pivot != pivot and down_pivot != minrev:
[email protected]afe30662011-07-30 01:05:52809 down_rev = revlist[down_pivot]
[email protected]53bb6342012-06-01 04:11:00810 down_fetch = DownloadJob(context, 'down_fetch', down_rev,
811 _GetDownloadPath(down_rev))
812 down_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25813
[email protected]eadd95d2012-11-02 22:42:09814 up_pivot = int((maxrev - pivot) / 2) + pivot
[email protected]53bb6342012-06-01 04:11:00815 up_fetch = None
[email protected]eadd95d2012-11-02 22:42:09816 if up_pivot != pivot and up_pivot != maxrev:
[email protected]afe30662011-07-30 01:05:52817 up_rev = revlist[up_pivot]
[email protected]53bb6342012-06-01 04:11:00818 up_fetch = DownloadJob(context, 'up_fetch', up_rev,
819 _GetDownloadPath(up_rev))
820 up_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25821
[email protected]afe30662011-07-30 01:05:52822 # Run test on the pivot revision.
skobes21b5cdfb2016-03-21 23:13:02823 exit_status = None
[email protected]e29c08c2012-09-17 20:50:50824 stdout = None
825 stderr = None
826 try:
skobes21b5cdfb2016-03-21 23:13:02827 (exit_status, stdout, stderr) = RunRevision(
828 context, rev, fetch.zip_file, profile, num_runs, command, try_args)
[email protected]e29c08c2012-09-17 20:50:50829 except Exception, e:
[email protected]fc3702e2013-11-09 04:23:00830 print >> sys.stderr, e
[email protected]60ac66e32011-07-18 16:08:25831
[email protected]53bb6342012-06-01 04:11:00832 # Call the evaluate function to see if the current revision is good or bad.
[email protected]afe30662011-07-30 01:05:52833 # On that basis, kill one of the background downloads and complete the
834 # other, as described in the comments above.
835 try:
Jason Kersey97bb027a2016-05-11 20:10:43836 answer = evaluate(rev, exit_status, stdout, stderr)
[email protected]4df583c2014-07-31 17:11:55837 if ((answer == 'g' and good_rev < bad_rev)
838 or (answer == 'b' and bad_rev < good_rev)):
[email protected]1d4a06242013-08-20 22:53:12839 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09840 minrev = pivot
[email protected]53bb6342012-06-01 04:11:00841 if down_fetch:
842 down_fetch.Stop() # Kill the download of the older revision.
[email protected]1d4a06242013-08-20 22:53:12843 fetch = None
[email protected]53bb6342012-06-01 04:11:00844 if up_fetch:
845 up_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52846 pivot = up_pivot
[email protected]eadd95d2012-11-02 22:42:09847 fetch = up_fetch
[email protected]4df583c2014-07-31 17:11:55848 elif ((answer == 'b' and good_rev < bad_rev)
849 or (answer == 'g' and bad_rev < good_rev)):
[email protected]1d4a06242013-08-20 22:53:12850 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09851 maxrev = pivot
[email protected]53bb6342012-06-01 04:11:00852 if up_fetch:
853 up_fetch.Stop() # Kill the download of the newer revision.
[email protected]1d4a06242013-08-20 22:53:12854 fetch = None
[email protected]53bb6342012-06-01 04:11:00855 if down_fetch:
856 down_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52857 pivot = down_pivot
[email protected]eadd95d2012-11-02 22:42:09858 fetch = down_fetch
[email protected]1d4a06242013-08-20 22:53:12859 elif answer == 'r':
860 pass # Retry requires no changes.
[email protected]53bb6342012-06-01 04:11:00861 elif answer == 'u':
862 # Nuke the revision from the revlist and choose a new pivot.
[email protected]1d4a06242013-08-20 22:53:12863 fetch.Stop()
[email protected]53bb6342012-06-01 04:11:00864 revlist.pop(pivot)
[email protected]eadd95d2012-11-02 22:42:09865 maxrev -= 1 # Assumes maxrev >= pivot.
[email protected]53bb6342012-06-01 04:11:00866
[email protected]eadd95d2012-11-02 22:42:09867 if maxrev - minrev > 1:
[email protected]53bb6342012-06-01 04:11:00868 # Alternate between using down_pivot or up_pivot for the new pivot
869 # point, without affecting the range. Do this instead of setting the
870 # pivot to the midpoint of the new range because adjacent revisions
871 # are likely affected by the same issue that caused the (u)nknown
872 # response.
873 if up_fetch and down_fetch:
874 fetch = [up_fetch, down_fetch][len(revlist) % 2]
875 elif up_fetch:
876 fetch = up_fetch
877 else:
878 fetch = down_fetch
879 fetch.WaitFor()
880 if fetch == up_fetch:
881 pivot = up_pivot - 1 # Subtracts 1 because revlist was resized.
882 else:
883 pivot = down_pivot
[email protected]53bb6342012-06-01 04:11:00884
885 if down_fetch and fetch != down_fetch:
886 down_fetch.Stop()
887 if up_fetch and fetch != up_fetch:
888 up_fetch.Stop()
889 else:
[email protected]4df583c2014-07-31 17:11:55890 assert False, 'Unexpected return value from evaluate(): ' + answer
skobes21b5cdfb2016-03-21 23:13:02891 except (KeyboardInterrupt, SystemExit):
[email protected]4df583c2014-07-31 17:11:55892 print 'Cleaning up...'
skobes21b5cdfb2016-03-21 23:13:02893 for f in [_GetDownloadPath(rev),
894 _GetDownloadPath(revlist[down_pivot]),
[email protected]5e93cf162012-01-28 02:16:56895 _GetDownloadPath(revlist[up_pivot])]:
[email protected]afe30662011-07-30 01:05:52896 try:
897 os.unlink(f)
898 except OSError:
899 pass
900 sys.exit(0)
901
902 rev = revlist[pivot]
903
[email protected]2e0f2672014-08-13 20:32:58904 return (revlist[minrev], revlist[maxrev], context)
[email protected]60ac66e32011-07-18 16:08:25905
906
pshenoycd6bd682014-09-10 20:50:22907def GetBlinkDEPSRevisionForChromiumRevision(self, rev):
[email protected]4c6fec6b2013-09-17 17:44:08908 """Returns the blink revision that was in REVISIONS file at
[email protected]b2fe7f22011-10-25 22:58:31909 chromium revision |rev|."""
pshenoycd6bd682014-09-10 20:50:22910
911 def _GetBlinkRev(url, blink_re):
912 m = blink_re.search(url.read())
913 url.close()
914 if m:
fmalitaa898d222016-07-12 22:29:03915 return m.group(1)
pshenoycd6bd682014-09-10 20:50:22916
Di Mu08c59682016-07-11 23:05:07917 url = urllib.urlopen(DEPS_FILE % GetGitHashFromSVNRevision(rev))
pshenoycd6bd682014-09-10 20:50:22918 if url.getcode() == 200:
Di Mu08c59682016-07-11 23:05:07919 blink_re = re.compile(r'webkit_revision\D*\d+;\D*\d+;(\w+)')
920 blink_git_sha = _GetBlinkRev(url, blink_re)
921 return self.GetSVNRevisionFromGitHash(blink_git_sha, 'blink')
pshenoycd6bd682014-09-10 20:50:22922 raise Exception('Could not get Blink revision for Chromium rev %d' % rev)
[email protected]37ed3172013-09-24 23:49:30923
924
[email protected]2e0f2672014-08-13 20:32:58925def GetBlinkRevisionForChromiumRevision(context, rev):
[email protected]37ed3172013-09-24 23:49:30926 """Returns the blink revision that was in REVISIONS file at
927 chromium revision |rev|."""
[email protected]3e7c85322014-06-27 20:27:36928 def _IsRevisionNumber(revision):
929 if isinstance(revision, int):
930 return True
931 else:
932 return revision.isdigit()
[email protected]2e0f2672014-08-13 20:32:58933 if str(rev) in context.githash_svn_dict:
934 rev = context.githash_svn_dict[str(rev)]
935 file_url = '%s/%s%s/REVISIONS' % (context.base_url,
936 context._listing_platform_dir, rev)
[email protected]4c6fec6b2013-09-17 17:44:08937 url = urllib.urlopen(file_url)
[email protected]2e0f2672014-08-13 20:32:58938 if url.getcode() == 200:
939 try:
940 data = json.loads(url.read())
941 except ValueError:
942 print 'ValueError for JSON URL: %s' % file_url
943 raise ValueError
944 else:
945 raise ValueError
[email protected]b2fe7f22011-10-25 22:58:31946 url.close()
[email protected]4c6fec6b2013-09-17 17:44:08947 if 'webkit_revision' in data:
[email protected]3e7c85322014-06-27 20:27:36948 blink_rev = data['webkit_revision']
949 if not _IsRevisionNumber(blink_rev):
[email protected]2e0f2672014-08-13 20:32:58950 blink_rev = int(context.GetSVNRevisionFromGitHash(blink_rev, 'blink'))
[email protected]3e7c85322014-06-27 20:27:36951 return blink_rev
[email protected]b2fe7f22011-10-25 22:58:31952 else:
[email protected]ff50d1c2013-04-17 18:49:36953 raise Exception('Could not get blink revision for cr rev %d' % rev)
[email protected]b2fe7f22011-10-25 22:58:31954
[email protected]4df583c2014-07-31 17:11:55955
[email protected]37ed3172013-09-24 23:49:30956def FixChromiumRevForBlink(revisions_final, revisions, self, rev):
957 """Returns the chromium revision that has the correct blink revision
958 for blink bisect, DEPS and REVISIONS file might not match since
959 blink snapshots point to tip of tree blink.
960 Note: The revisions_final variable might get modified to include
961 additional revisions."""
pshenoycd6bd682014-09-10 20:50:22962 blink_deps_rev = GetBlinkDEPSRevisionForChromiumRevision(self, rev)
[email protected]37ed3172013-09-24 23:49:30963
964 while (GetBlinkRevisionForChromiumRevision(self, rev) > blink_deps_rev):
965 idx = revisions.index(rev)
966 if idx > 0:
967 rev = revisions[idx-1]
968 if rev not in revisions_final:
969 revisions_final.insert(0, rev)
970
971 revisions_final.sort()
972 return rev
[email protected]b2fe7f22011-10-25 22:58:31973
[email protected]4df583c2014-07-31 17:11:55974
[email protected]5980b752014-07-02 00:34:40975def GetChromiumRevision(context, url):
[email protected]801fb652012-07-20 20:13:50976 """Returns the chromium revision read from given URL."""
977 try:
978 # Location of the latest build revision number
[email protected]5980b752014-07-02 00:34:40979 latest_revision = urllib.urlopen(url).read()
980 if latest_revision.isdigit():
981 return int(latest_revision)
982 return context.GetSVNRevisionFromGitHash(latest_revision)
[email protected]4df583c2014-07-31 17:11:55983 except Exception:
984 print 'Could not determine latest revision. This could be bad...'
[email protected]801fb652012-07-20 20:13:50985 return 999999999
986
pshenoycd6bd682014-09-10 20:50:22987def GetGitHashFromSVNRevision(svn_revision):
988 crrev_url = CRREV_URL + str(svn_revision)
989 url = urllib.urlopen(crrev_url)
990 if url.getcode() == 200:
991 data = json.loads(url.read())
992 if 'git_sha' in data:
993 return data['git_sha']
994
pshenoy9ce271f2014-09-02 22:14:05995def PrintChangeLog(min_chromium_rev, max_chromium_rev):
996 """Prints the changelog URL."""
997
pshenoycd6bd682014-09-10 20:50:22998 print (' ' + CHANGELOG_URL % (GetGitHashFromSVNRevision(min_chromium_rev),
999 GetGitHashFromSVNRevision(max_chromium_rev)))
pshenoy9ce271f2014-09-02 22:14:051000
[email protected]801fb652012-07-20 20:13:501001
[email protected]67e0bc62009-09-03 22:06:091002def main():
[email protected]2c1d2732009-10-29 19:52:171003 usage = ('%prog [options] [-- chromium-options]\n'
[email protected]887c9182013-02-12 20:30:311004 'Perform binary search on the snapshot builds to find a minimal\n'
1005 'range of revisions where a behavior change happened. The\n'
1006 'behaviors are described as "good" and "bad".\n'
1007 'It is NOT assumed that the behavior of the later revision is\n'
[email protected]09c58da2013-01-07 21:30:171008 'the bad one.\n'
[email protected]178aab72010-10-08 17:21:381009 '\n'
[email protected]887c9182013-02-12 20:30:311010 'Revision numbers should use\n'
[email protected]887c9182013-02-12 20:30:311011 ' SVN revisions (e.g. 123456) for chromium builds, from trunk.\n'
1012 ' Use base_trunk_revision from http://omahaproxy.appspot.com/\n'
1013 ' for earlier revs.\n'
1014 ' Chrome\'s about: build number and omahaproxy branch_revision\n'
1015 ' are incorrect, they are from branches.\n'
1016 '\n'
[email protected]178aab72010-10-08 17:21:381017 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
[email protected]7ad66a72009-09-04 17:52:331018 parser = optparse.OptionParser(usage=usage)
[email protected]1a45d222009-09-19 01:58:571019 # Strangely, the default help output doesn't include the choice list.
mikecasea8cd284c2014-12-02 21:30:581020 choices = ['mac', 'mac64', 'win', 'win64', 'linux', 'linux64', 'linux-arm',
dmazzoni76e907d2015-01-22 08:14:491021 'chromeos']
[email protected]7ad66a72009-09-04 17:52:331022 parser.add_option('-a', '--archive',
[email protected]4df583c2014-07-31 17:11:551023 choices=choices,
1024 help='The buildbot archive to bisect [%s].' %
1025 '|'.join(choices))
[email protected]4df583c2014-07-31 17:11:551026 parser.add_option('-b', '--bad',
1027 type='str',
1028 help='A bad revision to start bisection. '
1029 'May be earlier or later than the good revision. '
1030 'Default is HEAD.')
1031 parser.add_option('-f', '--flash_path',
1032 type='str',
1033 help='Absolute path to a recent Adobe Pepper Flash '
1034 'binary to be used in this bisection (e.g. '
1035 'on Windows C:\...\pepflashplayer.dll and on Linux '
1036 '/opt/google/chrome/PepperFlash/'
1037 'libpepflashplayer.so).')
[email protected]4df583c2014-07-31 17:11:551038 parser.add_option('-g', '--good',
1039 type='str',
1040 help='A good revision to start bisection. ' +
1041 'May be earlier or later than the bad revision. ' +
1042 'Default is 0.')
1043 parser.add_option('-p', '--profile', '--user-data-dir',
1044 type='str',
1045 default='profile',
1046 help='Profile to use; this will not reset every run. '
1047 'Defaults to a clean profile.')
1048 parser.add_option('-t', '--times',
1049 type='int',
1050 default=1,
1051 help='Number of times to run each build before asking '
1052 'if it\'s good or bad. Temporary profiles are reused.')
1053 parser.add_option('-c', '--command',
1054 type='str',
1055 default='%p %a',
1056 help='Command to execute. %p and %a refer to Chrome '
1057 'executable and specified extra arguments '
1058 'respectively. Use %s to specify all extra arguments '
1059 'as one string. Defaults to "%p %a". Note that any '
1060 'extra paths specified should be absolute.')
1061 parser.add_option('-l', '--blink',
1062 action='store_true',
1063 help='Use Blink bisect instead of Chromium. ')
1064 parser.add_option('', '--not-interactive',
1065 action='store_true',
1066 default=False,
1067 help='Use command exit code to tell good/bad revision.')
[email protected]011886692014-08-01 21:00:211068 parser.add_option('--asan',
1069 dest='asan',
1070 action='store_true',
1071 default=False,
1072 help='Allow the script to bisect ASAN builds')
rob724c9062015-01-22 00:26:421073 parser.add_option('--use-local-cache',
1074 dest='use_local_cache',
[email protected]6a7a5d62014-07-09 04:45:501075 action='store_true',
1076 default=False,
rob724c9062015-01-22 00:26:421077 help='Use a local file in the current directory to cache '
1078 'a list of known revisions to speed up the '
1079 'initialization of this script.')
skobes21b5cdfb2016-03-21 23:13:021080 parser.add_option('--verify-range',
1081 dest='verify_range',
1082 action='store_true',
1083 default=False,
1084 help='Test the first and last revisions in the range ' +
1085 'before proceeding with the bisect.')
[email protected]b3b20512013-08-26 18:51:041086
[email protected]7ad66a72009-09-04 17:52:331087 (opts, args) = parser.parse_args()
1088
1089 if opts.archive is None:
[email protected]178aab72010-10-08 17:21:381090 print 'Error: missing required parameter: --archive'
1091 print
[email protected]7ad66a72009-09-04 17:52:331092 parser.print_help()
1093 return 1
1094
[email protected]011886692014-08-01 21:00:211095 if opts.asan:
1096 supported_platforms = ['linux', 'mac', 'win']
1097 if opts.archive not in supported_platforms:
1098 print 'Error: ASAN bisecting only supported on these platforms: [%s].' % (
1099 '|'.join(supported_platforms))
1100 return 1
[email protected]011886692014-08-01 21:00:211101
1102 if opts.asan:
1103 base_url = ASAN_BASE_URL
1104 elif opts.blink:
[email protected]4c6fec6b2013-09-17 17:44:081105 base_url = WEBKIT_BASE_URL
1106 else:
1107 base_url = CHROMIUM_BASE_URL
1108
[email protected]183706d92011-06-10 13:06:221109 # Create the context. Initialize 0 for the revisions as they are set below.
[email protected]2e0f2672014-08-13 20:32:581110 context = PathContext(base_url, opts.archive, opts.good, opts.bad,
Jason Kersey97bb027a2016-05-11 20:10:431111 opts.asan, opts.use_local_cache,
vitalybuka4d1e1e412015-07-06 17:21:061112 opts.flash_path)
mikecasea8cd284c2014-12-02 21:30:581113
[email protected]67e0bc62009-09-03 22:06:091114 # Pick a starting point, try to get HEAD for this.
[email protected]2e0f2672014-08-13 20:32:581115 if not opts.bad:
1116 context.bad_revision = '999.0.0.0'
1117 context.bad_revision = GetChromiumRevision(
1118 context, context.GetLastChangeURL())
[email protected]67e0bc62009-09-03 22:06:091119
1120 # Find out when we were good.
[email protected]2e0f2672014-08-13 20:32:581121 if not opts.good:
Jason Kersey97bb027a2016-05-11 20:10:431122 context.good_revision = 0
[email protected]801fb652012-07-20 20:13:501123
[email protected]fc3702e2013-11-09 04:23:001124 if opts.flash_path:
[email protected]2e0f2672014-08-13 20:32:581125 msg = 'Could not find Flash binary at %s' % opts.flash_path
1126 assert os.path.exists(opts.flash_path), msg
[email protected]fc3702e2013-11-09 04:23:001127
Jason Kersey97bb027a2016-05-11 20:10:431128 context.good_revision = int(context.good_revision)
1129 context.bad_revision = int(context.bad_revision)
[email protected]801fb652012-07-20 20:13:501130
[email protected]5e93cf162012-01-28 02:16:561131 if opts.times < 1:
1132 print('Number of times to run (%d) must be greater than or equal to 1.' %
1133 opts.times)
1134 parser.print_help()
1135 return 1
1136
skobes21b5cdfb2016-03-21 23:13:021137 if opts.not_interactive:
1138 evaluator = DidCommandSucceed
1139 elif opts.asan:
[email protected]011886692014-08-01 21:00:211140 evaluator = IsGoodASANBuild
1141 else:
1142 evaluator = AskIsGoodBuild
1143
[email protected]2e0f2672014-08-13 20:32:581144 # Save these revision numbers to compare when showing the changelog URL
1145 # after the bisect.
1146 good_rev = context.good_revision
1147 bad_rev = context.bad_revision
1148
1149 (min_chromium_rev, max_chromium_rev, context) = Bisect(
1150 context, opts.times, opts.command, args, opts.profile,
skobes21b5cdfb2016-03-21 23:13:021151 evaluator, opts.verify_range)
[email protected]67e0bc62009-09-03 22:06:091152
[email protected]ff50d1c2013-04-17 18:49:361153 # Get corresponding blink revisions.
[email protected]b2fe7f22011-10-25 22:58:311154 try:
[email protected]4c6fec6b2013-09-17 17:44:081155 min_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1156 min_chromium_rev)
1157 max_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1158 max_chromium_rev)
[email protected]4df583c2014-07-31 17:11:551159 except Exception:
[email protected]b2fe7f22011-10-25 22:58:311160 # Silently ignore the failure.
[email protected]ff50d1c2013-04-17 18:49:361161 min_blink_rev, max_blink_rev = 0, 0
[email protected]b2fe7f22011-10-25 22:58:311162
[email protected]3bdaa4752013-09-30 20:13:361163 if opts.blink:
1164 # We're done. Let the user know the results in an official manner.
1165 if good_rev > bad_rev:
1166 print DONE_MESSAGE_GOOD_MAX % (str(min_blink_rev), str(max_blink_rev))
1167 else:
1168 print DONE_MESSAGE_GOOD_MIN % (str(min_blink_rev), str(max_blink_rev))
[email protected]eadd95d2012-11-02 22:42:091169
[email protected]ff50d1c2013-04-17 18:49:361170 print 'BLINK CHANGELOG URL:'
1171 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev)
[email protected]3bdaa4752013-09-30 20:13:361172
[email protected]d0149c5c2012-05-29 21:12:111173 else:
[email protected]3bdaa4752013-09-30 20:13:361174 # We're done. Let the user know the results in an official manner.
1175 if good_rev > bad_rev:
1176 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev),
1177 str(max_chromium_rev))
1178 else:
1179 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev),
1180 str(max_chromium_rev))
1181 if min_blink_rev != max_blink_rev:
[email protected]4df583c2014-07-31 17:11:551182 print ('NOTE: There is a Blink roll in the range, '
1183 'you might also want to do a Blink bisect.')
[email protected]3bdaa4752013-09-30 20:13:361184
1185 print 'CHANGELOG URL:'
Jason Kersey97bb027a2016-05-11 20:10:431186 PrintChangeLog(min_chromium_rev, max_chromium_rev)
[email protected]cb155a82011-11-29 17:25:341187
[email protected]4df583c2014-07-31 17:11:551188
[email protected]67e0bc62009-09-03 22:06:091189if __name__ == '__main__':
[email protected]7ad66a72009-09-04 17:52:331190 sys.exit(main())